From 2ecf12f7275ddd2cbc6b6c39d43754c67200c909 Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Wed, 4 Dec 2024 23:17:36 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...米便签开源代码的泛读报告.docx | Bin 0 -> 16698 bytes ...签开源代码的质量分析报告.docx | Bin 0 -> 13911 bytes src/net/micode/notes/data/Contact.java | 73 ++ src/net/micode/notes/data/Notes.java | 279 +++++ .../notes/data/NotesDatabaseHelper.java | 362 +++++++ src/net/micode/notes/data/NotesProvider.java | 305 ++++++ src/net/micode/notes/gtask/data/MetaData.java | 82 ++ src/net/micode/notes/gtask/data/Node.java | 101 ++ src/net/micode/notes/gtask/data/SqlData.java | 189 ++++ src/net/micode/notes/gtask/data/SqlNote.java | 505 +++++++++ src/net/micode/notes/gtask/data/Task.java | 351 +++++++ src/net/micode/notes/gtask/data/TaskList.java | 343 +++++++ .../exception/ActionFailureException.java | 33 + .../exception/NetworkFailureException.java | 33 + .../notes/gtask/remote/GTaskASyncTask.java | 123 +++ .../notes/gtask/remote/GTaskClient.java | 585 +++++++++++ .../notes/gtask/remote/GTaskManager.java | 800 +++++++++++++++ .../notes/gtask/remote/GTaskSyncService.java | 128 +++ src/net/micode/notes/model/Note.java | 253 +++++ src/net/micode/notes/model/WorkingNote.java | 368 +++++++ src/net/micode/notes/tool/BackupUtils.java | 344 +++++++ src/net/micode/notes/tool/DataUtils.java | 295 ++++++ .../micode/notes/tool/GTaskStringUtils.java | 113 +++ src/net/micode/notes/tool/ResourceParser.java | 181 ++++ .../micode/notes/ui/AlarmAlertActivity.java | 158 +++ .../micode/notes/ui/AlarmInitReceiver.java | 65 ++ src/net/micode/notes/ui/AlarmReceiver.java | 30 + src/net/micode/notes/ui/DateTimePicker.java | 485 +++++++++ .../micode/notes/ui/DateTimePickerDialog.java | 90 ++ src/net/micode/notes/ui/DropdownMenu.java | 61 ++ .../micode/notes/ui/FoldersListAdapter.java | 80 ++ src/net/micode/notes/ui/NoteEditActivity.java | 873 ++++++++++++++++ src/net/micode/notes/ui/NoteEditText.java | 217 ++++ src/net/micode/notes/ui/NoteItemData.java | 224 ++++ .../micode/notes/ui/NotesListActivity.java | 954 ++++++++++++++++++ src/net/micode/notes/ui/NotesListAdapter.java | 184 ++++ src/net/micode/notes/ui/NotesListItem.java | 122 +++ .../notes/ui/NotesPreferenceActivity.java | 388 +++++++ .../notes/widget/NoteWidgetProvider.java | 132 +++ .../notes/widget/NoteWidgetProvider_2x.java | 47 + .../notes/widget/NoteWidgetProvider_4x.java | 46 + 41 files changed, 10002 insertions(+) create mode 100644 doc/小米便签开源代码的泛读报告.docx create mode 100644 doc/小米便签开源代码的质量分析报告.docx create mode 100644 src/net/micode/notes/data/Contact.java create mode 100644 src/net/micode/notes/data/Notes.java create mode 100644 src/net/micode/notes/data/NotesDatabaseHelper.java create mode 100644 src/net/micode/notes/data/NotesProvider.java create mode 100644 src/net/micode/notes/gtask/data/MetaData.java create mode 100644 src/net/micode/notes/gtask/data/Node.java create mode 100644 src/net/micode/notes/gtask/data/SqlData.java create mode 100644 src/net/micode/notes/gtask/data/SqlNote.java create mode 100644 src/net/micode/notes/gtask/data/Task.java create mode 100644 src/net/micode/notes/gtask/data/TaskList.java create mode 100644 src/net/micode/notes/gtask/exception/ActionFailureException.java create mode 100644 src/net/micode/notes/gtask/exception/NetworkFailureException.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskASyncTask.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskClient.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskManager.java create mode 100644 src/net/micode/notes/gtask/remote/GTaskSyncService.java create mode 100644 src/net/micode/notes/model/Note.java create mode 100644 src/net/micode/notes/model/WorkingNote.java create mode 100644 src/net/micode/notes/tool/BackupUtils.java create mode 100644 src/net/micode/notes/tool/DataUtils.java create mode 100644 src/net/micode/notes/tool/GTaskStringUtils.java create mode 100644 src/net/micode/notes/tool/ResourceParser.java create mode 100644 src/net/micode/notes/ui/AlarmAlertActivity.java create mode 100644 src/net/micode/notes/ui/AlarmInitReceiver.java create mode 100644 src/net/micode/notes/ui/AlarmReceiver.java create mode 100644 src/net/micode/notes/ui/DateTimePicker.java create mode 100644 src/net/micode/notes/ui/DateTimePickerDialog.java create mode 100644 src/net/micode/notes/ui/DropdownMenu.java create mode 100644 src/net/micode/notes/ui/FoldersListAdapter.java create mode 100644 src/net/micode/notes/ui/NoteEditActivity.java create mode 100644 src/net/micode/notes/ui/NoteEditText.java create mode 100644 src/net/micode/notes/ui/NoteItemData.java create mode 100644 src/net/micode/notes/ui/NotesListActivity.java create mode 100644 src/net/micode/notes/ui/NotesListAdapter.java create mode 100644 src/net/micode/notes/ui/NotesListItem.java create mode 100644 src/net/micode/notes/ui/NotesPreferenceActivity.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider_2x.java create mode 100644 src/net/micode/notes/widget/NoteWidgetProvider_4x.java diff --git a/doc/小米便签开源代码的泛读报告.docx b/doc/小米便签开源代码的泛读报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..92cdf698ed0aae8d6f97df93d97d27e6fe08e660 GIT binary patch literal 16698 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 F{{gUsbC>`C literal 0 HcmV?d00001 diff --git a/doc/小米便签开源代码的质量分析报告.docx b/doc/小米便签开源代码的质量分析报告.docx new file mode 100644 index 0000000000000000000000000000000000000000..45f911a6901431655901987f1bdf9f2d53d75f8f GIT binary patch literal 13911 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 literal 0 HcmV?d00001 diff --git a/src/net/micode/notes/data/Contact.java b/src/net/micode/notes/data/Contact.java new file mode 100644 index 0000000..d97ac5d --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/data/Notes.java b/src/net/micode/notes/data/Notes.java new file mode 100644 index 0000000..f240604 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/net/micode/notes/data/NotesDatabaseHelper.java new file mode 100644 index 0000000..ffe5d57 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/data/NotesProvider.java b/src/net/micode/notes/data/NotesProvider.java new file mode 100644 index 0000000..edb0a60 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java new file mode 100644 index 0000000..3a2050b --- /dev/null +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -0,0 +1,82 @@ +/* + * 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.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + + +public class MetaData extends Task { + private final static String TAG = MetaData.class.getSimpleName(); + + private String mRelatedGid = null; + + public void setMeta(String gid, JSONObject metaInfo) { + try { + metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); + } catch (JSONException e) { + Log.e(TAG, "failed to put related gid"); + } + setNotes(metaInfo.toString()); + setName(GTaskStringUtils.META_NOTE_NAME); + } + + public String getRelatedGid() { + return mRelatedGid; + } + + @Override + public boolean isWorthSaving() { + return getNotes() != null; + } + + @Override + public void setContentByRemoteJSON(JSONObject js) { + super.setContentByRemoteJSON(js); + if (getNotes() != null) { + try { + JSONObject metaInfo = new JSONObject(getNotes().trim()); + mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); + } catch (JSONException e) { + Log.w(TAG, "failed to get related gid"); + mRelatedGid = null; + } + } + } + + @Override + public void setContentByLocalJSON(JSONObject js) { + // this function should not be called + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); + } + + @Override + public JSONObject getLocalJSONFromContent() { + throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); + } + + @Override + public int getSyncAction(Cursor c) { + throw new IllegalAccessError("MetaData:getSyncAction should not be called"); + } + +} diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java new file mode 100644 index 0000000..63950e0 --- /dev/null +++ b/src/net/micode/notes/gtask/data/Node.java @@ -0,0 +1,101 @@ +/* + * 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.gtask.data; + +import android.database.Cursor; + +import org.json.JSONObject; + +public abstract class Node { + public static final int SYNC_ACTION_NONE = 0; + + public static final int SYNC_ACTION_ADD_REMOTE = 1; + + public static final int SYNC_ACTION_ADD_LOCAL = 2; + + public static final int SYNC_ACTION_DEL_REMOTE = 3; + + public static final int SYNC_ACTION_DEL_LOCAL = 4; + + public static final int SYNC_ACTION_UPDATE_REMOTE = 5; + + public static final int SYNC_ACTION_UPDATE_LOCAL = 6; + + public static final int SYNC_ACTION_UPDATE_CONFLICT = 7; + + public static final int SYNC_ACTION_ERROR = 8; + + private String mGid; + + private String mName; + + private long mLastModified; + + private boolean mDeleted; + + public Node() { + mGid = null; + mName = ""; + mLastModified = 0; + mDeleted = false; + } + + public abstract JSONObject getCreateAction(int actionId); + + public abstract JSONObject getUpdateAction(int actionId); + + public abstract void setContentByRemoteJSON(JSONObject js); + + public abstract void setContentByLocalJSON(JSONObject js); + + public abstract JSONObject getLocalJSONFromContent(); + + public abstract int getSyncAction(Cursor c); + + public void setGid(String gid) { + this.mGid = gid; + } + + public void setName(String name) { + this.mName = name; + } + + public void setLastModified(long lastModified) { + this.mLastModified = lastModified; + } + + public void setDeleted(boolean deleted) { + this.mDeleted = deleted; + } + + public String getGid() { + return this.mGid; + } + + public String getName() { + return this.mName; + } + + public long getLastModified() { + return this.mLastModified; + } + + public boolean getDeleted() { + return this.mDeleted; + } + +} diff --git a/src/net/micode/notes/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java new file mode 100644 index 0000000..d3ec3be --- /dev/null +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -0,0 +1,189 @@ +/* + * 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.gtask.data; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +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 net.micode.notes.data.NotesDatabaseHelper.TABLE; +import net.micode.notes.gtask.exception.ActionFailureException; + +import org.json.JSONException; +import org.json.JSONObject; + + +public class SqlData { + private static final String TAG = SqlData.class.getSimpleName(); + + private static final int INVALID_ID = -99999; + + public static final String[] PROJECTION_DATA = new String[] { + DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1, + DataColumns.DATA3 + }; + + public static final int DATA_ID_COLUMN = 0; + + public static final int DATA_MIME_TYPE_COLUMN = 1; + + public static final int DATA_CONTENT_COLUMN = 2; + + public static final int DATA_CONTENT_DATA_1_COLUMN = 3; + + public static final int DATA_CONTENT_DATA_3_COLUMN = 4; + + private ContentResolver mContentResolver; + + private boolean mIsCreate; + + private long mDataId; + + private String mDataMimeType; + + private String mDataContent; + + private long mDataContentData1; + + private String mDataContentData3; + + private ContentValues mDiffDataValues; + + public SqlData(Context context) { + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mDataId = INVALID_ID; + mDataMimeType = DataConstants.NOTE; + mDataContent = ""; + mDataContentData1 = 0; + mDataContentData3 = ""; + mDiffDataValues = new ContentValues(); + } + + public SqlData(Context context, Cursor c) { + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDiffDataValues = new ContentValues(); + } + + private void loadFromCursor(Cursor c) { + mDataId = c.getLong(DATA_ID_COLUMN); + mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); + mDataContent = c.getString(DATA_CONTENT_COLUMN); + mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN); + mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN); + } + + public void setContent(JSONObject js) throws JSONException { + long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; + if (mIsCreate || mDataId != dataId) { + mDiffDataValues.put(DataColumns.ID, dataId); + } + mDataId = dataId; + + String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE) + : DataConstants.NOTE; + if (mIsCreate || !mDataMimeType.equals(dataMimeType)) { + mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType); + } + mDataMimeType = dataMimeType; + + String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : ""; + if (mIsCreate || !mDataContent.equals(dataContent)) { + mDiffDataValues.put(DataColumns.CONTENT, dataContent); + } + mDataContent = dataContent; + + long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0; + if (mIsCreate || mDataContentData1 != dataContentData1) { + mDiffDataValues.put(DataColumns.DATA1, dataContentData1); + } + mDataContentData1 = dataContentData1; + + String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : ""; + if (mIsCreate || !mDataContentData3.equals(dataContentData3)) { + mDiffDataValues.put(DataColumns.DATA3, dataContentData3); + } + mDataContentData3 = dataContentData3; + } + + public JSONObject getContent() throws JSONException { + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + JSONObject js = new JSONObject(); + js.put(DataColumns.ID, mDataId); + js.put(DataColumns.MIME_TYPE, mDataMimeType); + js.put(DataColumns.CONTENT, mDataContent); + js.put(DataColumns.DATA1, mDataContentData1); + js.put(DataColumns.DATA3, mDataContentData3); + return js; + } + + public void commit(long noteId, boolean validateVersion, long version) { + + if (mIsCreate) { + if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { + mDiffDataValues.remove(DataColumns.ID); + } + + mDiffDataValues.put(DataColumns.NOTE_ID, noteId); + Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues); + try { + mDataId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + } else { + if (mDiffDataValues.size() > 0) { + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null); + } else { + result = mContentResolver.update(ContentUris.withAppendedId( + Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, + " ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE + + " WHERE " + NoteColumns.VERSION + "=?)", new String[] { + String.valueOf(noteId), String.valueOf(version) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + } + + mDiffDataValues.clear(); + mIsCreate = false; + } + + public long getId() { + return mDataId; + } +} diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java new file mode 100644 index 0000000..79a4095 --- /dev/null +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -0,0 +1,505 @@ +/* + * 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.gtask.data; + +import android.appwidget.AppWidgetManager; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.tool.ResourceParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class SqlNote { + private static final String TAG = SqlNote.class.getSimpleName(); + + private static final int INVALID_ID = -99999; + + public static final String[] PROJECTION_NOTE = 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.SYNC_ID, + NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID, + NoteColumns.VERSION + }; + + public static final int ID_COLUMN = 0; + + public static final int ALERTED_DATE_COLUMN = 1; + + public static final int BG_COLOR_ID_COLUMN = 2; + + public static final int CREATED_DATE_COLUMN = 3; + + public static final int HAS_ATTACHMENT_COLUMN = 4; + + public static final int MODIFIED_DATE_COLUMN = 5; + + public static final int NOTES_COUNT_COLUMN = 6; + + public static final int PARENT_ID_COLUMN = 7; + + public static final int SNIPPET_COLUMN = 8; + + public static final int TYPE_COLUMN = 9; + + public static final int WIDGET_ID_COLUMN = 10; + + public static final int WIDGET_TYPE_COLUMN = 11; + + public static final int SYNC_ID_COLUMN = 12; + + public static final int LOCAL_MODIFIED_COLUMN = 13; + + public static final int ORIGIN_PARENT_ID_COLUMN = 14; + + public static final int GTASK_ID_COLUMN = 15; + + public static final int VERSION_COLUMN = 16; + + private Context mContext; + + private ContentResolver mContentResolver; + + private boolean mIsCreate; + + private long mId; + + private long mAlertDate; + + private int mBgColorId; + + private long mCreatedDate; + + private int mHasAttachment; + + private long mModifiedDate; + + private long mParentId; + + private String mSnippet; + + private int mType; + + private int mWidgetId; + + private int mWidgetType; + + private long mOriginParent; + + private long mVersion; + + private ContentValues mDiffNoteValues; + + private ArrayList mDataList; + + public SqlNote(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = true; + mId = INVALID_ID; + mAlertDate = 0; + mBgColorId = ResourceParser.getDefaultBgId(context); + mCreatedDate = System.currentTimeMillis(); + mHasAttachment = 0; + mModifiedDate = System.currentTimeMillis(); + mParentId = 0; + mSnippet = ""; + mType = Notes.TYPE_NOTE; + mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; + mWidgetType = Notes.TYPE_WIDGET_INVALIDE; + mOriginParent = 0; + mVersion = 0; + mDiffNoteValues = new ContentValues(); + mDataList = new ArrayList(); + } + + public SqlNote(Context context, Cursor c) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(c); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + } + + public SqlNote(Context context, long id) { + mContext = context; + mContentResolver = context.getContentResolver(); + mIsCreate = false; + loadFromCursor(id); + mDataList = new ArrayList(); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + mDiffNoteValues = new ContentValues(); + + } + + private void loadFromCursor(long id) { + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(id) + }, null); + if (c != null) { + c.moveToNext(); + loadFromCursor(c); + } else { + Log.w(TAG, "loadFromCursor: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + private void loadFromCursor(Cursor c) { + mId = c.getLong(ID_COLUMN); + mAlertDate = c.getLong(ALERTED_DATE_COLUMN); + mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = c.getLong(CREATED_DATE_COLUMN); + mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); + mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); + mParentId = c.getLong(PARENT_ID_COLUMN); + mSnippet = c.getString(SNIPPET_COLUMN); + mType = c.getInt(TYPE_COLUMN); + mWidgetId = c.getInt(WIDGET_ID_COLUMN); + mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); + mVersion = c.getLong(VERSION_COLUMN); + } + + private void loadDataContent() { + Cursor c = null; + mDataList.clear(); + try { + c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA, + "(note_id=?)", new String[] { + String.valueOf(mId) + }, null); + if (c != null) { + if (c.getCount() == 0) { + Log.w(TAG, "it seems that the note has not data"); + return; + } + while (c.moveToNext()) { + SqlData data = new SqlData(mContext, c); + mDataList.add(data); + } + } else { + Log.w(TAG, "loadDataContent: cursor = null"); + } + } finally { + if (c != null) + c.close(); + } + } + + public boolean setContent(JSONObject js) { + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + Log.w(TAG, "cannot set system folder"); + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + // for folder we can only update the snnipet and type + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + } else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID; + if (mIsCreate || mId != id) { + mDiffNoteValues.put(NoteColumns.ID, id); + } + mId = id; + + long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note + .getLong(NoteColumns.ALERTED_DATE) : 0; + if (mIsCreate || mAlertDate != alertDate) { + mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate); + } + mAlertDate = alertDate; + + int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note + .getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext); + if (mIsCreate || mBgColorId != bgColorId) { + mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId); + } + mBgColorId = bgColorId; + + long createDate = note.has(NoteColumns.CREATED_DATE) ? note + .getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mCreatedDate != createDate) { + mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate); + } + mCreatedDate = createDate; + + int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note + .getInt(NoteColumns.HAS_ATTACHMENT) : 0; + if (mIsCreate || mHasAttachment != hasAttachment) { + mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment); + } + mHasAttachment = hasAttachment; + + long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note + .getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis(); + if (mIsCreate || mModifiedDate != modifiedDate) { + mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate); + } + mModifiedDate = modifiedDate; + + long parentId = note.has(NoteColumns.PARENT_ID) ? note + .getLong(NoteColumns.PARENT_ID) : 0; + if (mIsCreate || mParentId != parentId) { + mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId); + } + mParentId = parentId; + + String snippet = note.has(NoteColumns.SNIPPET) ? note + .getString(NoteColumns.SNIPPET) : ""; + if (mIsCreate || !mSnippet.equals(snippet)) { + mDiffNoteValues.put(NoteColumns.SNIPPET, snippet); + } + mSnippet = snippet; + + int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE) + : Notes.TYPE_NOTE; + if (mIsCreate || mType != type) { + mDiffNoteValues.put(NoteColumns.TYPE, type); + } + mType = type; + + int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID) + : AppWidgetManager.INVALID_APPWIDGET_ID; + if (mIsCreate || mWidgetId != widgetId) { + mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId); + } + mWidgetId = widgetId; + + int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note + .getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE; + if (mIsCreate || mWidgetType != widgetType) { + mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType); + } + mWidgetType = widgetType; + + long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note + .getLong(NoteColumns.ORIGIN_PARENT_ID) : 0; + if (mIsCreate || mOriginParent != originParent) { + mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent); + } + mOriginParent = originParent; + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + SqlData sqlData = null; + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + for (SqlData temp : mDataList) { + if (dataId == temp.getId()) { + sqlData = temp; + } + } + } + + if (sqlData == null) { + sqlData = new SqlData(mContext); + mDataList.add(sqlData); + } + + sqlData.setContent(data); + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } + return true; + } + + public JSONObject getContent() { + try { + JSONObject js = new JSONObject(); + + if (mIsCreate) { + Log.e(TAG, "it seems that we haven't created this in database yet"); + return null; + } + + JSONObject note = new JSONObject(); + if (mType == Notes.TYPE_NOTE) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.ALERTED_DATE, mAlertDate); + note.put(NoteColumns.BG_COLOR_ID, mBgColorId); + note.put(NoteColumns.CREATED_DATE, mCreatedDate); + note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment); + note.put(NoteColumns.MODIFIED_DATE, mModifiedDate); + note.put(NoteColumns.PARENT_ID, mParentId); + note.put(NoteColumns.SNIPPET, mSnippet); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.WIDGET_ID, mWidgetId); + note.put(NoteColumns.WIDGET_TYPE, mWidgetType); + note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + + JSONArray dataArray = new JSONArray(); + for (SqlData sqlData : mDataList) { + JSONObject data = sqlData.getContent(); + if (data != null) { + dataArray.put(data); + } + } + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + } else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) { + note.put(NoteColumns.ID, mId); + note.put(NoteColumns.TYPE, mType); + note.put(NoteColumns.SNIPPET, mSnippet); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + } + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + return null; + } + + public void setParentId(long id) { + mParentId = id; + mDiffNoteValues.put(NoteColumns.PARENT_ID, id); + } + + public void setGtaskId(String gid) { + mDiffNoteValues.put(NoteColumns.GTASK_ID, gid); + } + + public void setSyncId(long syncId) { + mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId); + } + + public void resetLocalModified() { + mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0); + } + + public long getId() { + return mId; + } + + public long getParentId() { + return mParentId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean isNoteType() { + return mType == Notes.TYPE_NOTE; + } + + public void commit(boolean validateVersion) { + if (mIsCreate) { + if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) { + mDiffNoteValues.remove(NoteColumns.ID); + } + + Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues); + try { + mId = Long.valueOf(uri.getPathSegments().get(1)); + } catch (NumberFormatException e) { + Log.e(TAG, "Get note id error :" + e.toString()); + throw new ActionFailureException("create note failed"); + } + if (mId == 0) { + throw new IllegalStateException("Create thread id failed"); + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, false, -1); + } + } + } else { + if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) { + Log.e(TAG, "No such note"); + throw new IllegalStateException("Try to update note with invalid id"); + } + if (mDiffNoteValues.size() > 0) { + mVersion ++; + int result = 0; + if (!validateVersion) { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?)", new String[] { + String.valueOf(mId) + }); + } else { + result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)", + new String[] { + String.valueOf(mId), String.valueOf(mVersion) + }); + } + if (result == 0) { + Log.w(TAG, "there is no update. maybe user updates note when syncing"); + } + } + + if (mType == Notes.TYPE_NOTE) { + for (SqlData sqlData : mDataList) { + sqlData.commit(mId, validateVersion, mVersion); + } + } + } + + // refresh local info + loadFromCursor(mId); + if (mType == Notes.TYPE_NOTE) + loadDataContent(); + + mDiffNoteValues.clear(); + mIsCreate = false; + } +} diff --git a/src/net/micode/notes/gtask/data/Task.java b/src/net/micode/notes/gtask/data/Task.java new file mode 100644 index 0000000..6a19454 --- /dev/null +++ b/src/net/micode/notes/gtask/data/Task.java @@ -0,0 +1,351 @@ +/* + * 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.gtask.data; + +import android.database.Cursor; +import android.text.TextUtils; +import android.util.Log; + +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 net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class Task extends Node { + private static final String TAG = Task.class.getSimpleName(); + + private boolean mCompleted; + + private String mNotes; + + private JSONObject mMetaInfo; + + private Task mPriorSibling; + + private TaskList mParent; + + public Task() { + super(); + mCompleted = false; + mNotes = null; + mPriorSibling = null; + mParent = null; + mMetaInfo = null; + } + + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this)); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_TASK); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + // parent_id + js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid()); + + // dest_parent_type + js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + + // list_id + js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid()); + + // prior_sibling_id + if (mPriorSibling != null) { + js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid()); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-create jsonobject"); + } + + return js; + } + + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + if (getNotes() != null) { + entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes()); + } + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate task-update jsonobject"); + } + + return js; + } + + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + // notes + if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) { + setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES)); + } + + // deleted + if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) { + setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED)); + } + + // completed + if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) { + setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED)); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get task content from jsonobject"); + } + } + } + + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE) + || !js.has(GTaskStringUtils.META_HEAD_DATA)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) { + Log.e(TAG, "invalid type"); + return; + } + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + setName(data.getString(DataColumns.CONTENT)); + break; + } + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + public JSONObject getLocalJSONFromContent() { + String name = getName(); + try { + if (mMetaInfo == null) { + // new task created from web + if (name == null) { + Log.w(TAG, "the note seems to be an empty one"); + return null; + } + + JSONObject js = new JSONObject(); + JSONObject note = new JSONObject(); + JSONArray dataArray = new JSONArray(); + JSONObject data = new JSONObject(); + data.put(DataColumns.CONTENT, name); + dataArray.put(data); + js.put(GTaskStringUtils.META_HEAD_DATA, dataArray); + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + js.put(GTaskStringUtils.META_HEAD_NOTE, note); + return js; + } else { + // synced task + JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) { + data.put(DataColumns.CONTENT, getName()); + break; + } + } + + note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + return mMetaInfo; + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + public void setMetaInfo(MetaData metaData) { + if (metaData != null && metaData.getNotes() != null) { + try { + mMetaInfo = new JSONObject(metaData.getNotes()); + } catch (JSONException e) { + Log.w(TAG, e.toString()); + mMetaInfo = null; + } + } + } + + public int getSyncAction(Cursor c) { + try { + JSONObject noteInfo = null; + if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) { + noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + } + + if (noteInfo == null) { + Log.w(TAG, "it seems that note meta has been deleted"); + return SYNC_ACTION_UPDATE_REMOTE; + } + + if (!noteInfo.has(NoteColumns.ID)) { + Log.w(TAG, "remote note id seems to be deleted"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + // validate the note id now + if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) { + Log.w(TAG, "note id doesn't match"); + return SYNC_ACTION_UPDATE_LOCAL; + } + + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + return SYNC_ACTION_UPDATE_CONFLICT; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + public boolean isWorthSaving() { + return mMetaInfo != null || (getName() != null && getName().trim().length() > 0) + || (getNotes() != null && getNotes().trim().length() > 0); + } + + public void setCompleted(boolean completed) { + this.mCompleted = completed; + } + + public void setNotes(String notes) { + this.mNotes = notes; + } + + public void setPriorSibling(Task priorSibling) { + this.mPriorSibling = priorSibling; + } + + public void setParent(TaskList parent) { + this.mParent = parent; + } + + public boolean getCompleted() { + return this.mCompleted; + } + + public String getNotes() { + return this.mNotes; + } + + public Task getPriorSibling() { + return this.mPriorSibling; + } + + public TaskList getParent() { + return this.mParent; + } + +} diff --git a/src/net/micode/notes/gtask/data/TaskList.java b/src/net/micode/notes/gtask/data/TaskList.java new file mode 100644 index 0000000..4ea21c5 --- /dev/null +++ b/src/net/micode/notes/gtask/data/TaskList.java @@ -0,0 +1,343 @@ +/* + * 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.gtask.data; + +import android.database.Cursor; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + + +public class TaskList extends Node { + private static final String TAG = TaskList.class.getSimpleName(); + + private int mIndex; + + private ArrayList mChildren; + + public TaskList() { + super(); + mChildren = new ArrayList(); + mIndex = 1; + } + + public JSONObject getCreateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // index + js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); + entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE, + GTaskStringUtils.GTASK_JSON_TYPE_GROUP); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-create jsonobject"); + } + + return js; + } + + public JSONObject getUpdateAction(int actionId) { + JSONObject js = new JSONObject(); + + try { + // action_type + js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE); + + // action_id + js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId); + + // id + js.put(GTaskStringUtils.GTASK_JSON_ID, getGid()); + + // entity_delta + JSONObject entity = new JSONObject(); + entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); + entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted()); + js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to generate tasklist-update jsonobject"); + } + + return js; + } + + public void setContentByRemoteJSON(JSONObject js) { + if (js != null) { + try { + // id + if (js.has(GTaskStringUtils.GTASK_JSON_ID)) { + setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID)); + } + + // last_modified + if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) { + setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)); + } + + // name + if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) { + setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME)); + } + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("fail to get tasklist content from jsonobject"); + } + } + } + + public void setContentByLocalJSON(JSONObject js) { + if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) { + Log.w(TAG, "setContentByLocalJSON: nothing is avaiable"); + } + + try { + JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + + if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) { + String name = folder.getString(NoteColumns.SNIPPET); + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name); + } else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) { + if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT); + else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) + setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE); + else + Log.e(TAG, "invalid system folder"); + } else { + Log.e(TAG, "error type"); + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + } + + public JSONObject getLocalJSONFromContent() { + try { + JSONObject js = new JSONObject(); + JSONObject folder = new JSONObject(); + + String folderName = getName(); + if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) + folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(), + folderName.length()); + folder.put(NoteColumns.SNIPPET, folderName); + if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT) + || folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) + folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + else + folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + + js.put(GTaskStringUtils.META_HEAD_NOTE, folder); + + return js; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return null; + } + } + + public int getSyncAction(Cursor c) { + try { + if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) { + // there is no local update + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // no update both side + return SYNC_ACTION_NONE; + } else { + // apply remote to local + return SYNC_ACTION_UPDATE_LOCAL; + } + } else { + // validate gtask id + if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) { + Log.e(TAG, "gtask id doesn't match"); + return SYNC_ACTION_ERROR; + } + if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) { + // local modification only + return SYNC_ACTION_UPDATE_REMOTE; + } else { + // for folder conflicts, just apply local modification + return SYNC_ACTION_UPDATE_REMOTE; + } + } + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + return SYNC_ACTION_ERROR; + } + + public int getChildTaskCount() { + return mChildren.size(); + } + + public boolean addChildTask(Task task) { + boolean ret = false; + if (task != null && !mChildren.contains(task)) { + ret = mChildren.add(task); + if (ret) { + // need to set prior sibling and parent + task.setPriorSibling(mChildren.isEmpty() ? null : mChildren + .get(mChildren.size() - 1)); + task.setParent(this); + } + } + return ret; + } + + public boolean addChildTask(Task task, int index) { + if (index < 0 || index > mChildren.size()) { + Log.e(TAG, "add child task: invalid index"); + return false; + } + + int pos = mChildren.indexOf(task); + if (task != null && pos == -1) { + mChildren.add(index, task); + + // update the task list + Task preTask = null; + Task afterTask = null; + if (index != 0) + preTask = mChildren.get(index - 1); + if (index != mChildren.size() - 1) + afterTask = mChildren.get(index + 1); + + task.setPriorSibling(preTask); + if (afterTask != null) + afterTask.setPriorSibling(task); + } + + return true; + } + + public boolean removeChildTask(Task task) { + boolean ret = false; + int index = mChildren.indexOf(task); + if (index != -1) { + ret = mChildren.remove(task); + + if (ret) { + // reset prior sibling and parent + task.setPriorSibling(null); + task.setParent(null); + + // update the task list + if (index != mChildren.size()) { + mChildren.get(index).setPriorSibling( + index == 0 ? null : mChildren.get(index - 1)); + } + } + } + return ret; + } + + public boolean moveChildTask(Task task, int index) { + + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "move child task: invalid index"); + return false; + } + + int pos = mChildren.indexOf(task); + if (pos == -1) { + Log.e(TAG, "move child task: the task should in the list"); + return false; + } + + if (pos == index) + return true; + return (removeChildTask(task) && addChildTask(task, index)); + } + + public Task findChildTaskByGid(String gid) { + for (int i = 0; i < mChildren.size(); i++) { + Task t = mChildren.get(i); + if (t.getGid().equals(gid)) { + return t; + } + } + return null; + } + + public int getChildTaskIndex(Task task) { + return mChildren.indexOf(task); + } + + public Task getChildTaskByIndex(int index) { + if (index < 0 || index >= mChildren.size()) { + Log.e(TAG, "getTaskByIndex: invalid index"); + return null; + } + return mChildren.get(index); + } + + public Task getChilTaskByGid(String gid) { + for (Task task : mChildren) { + if (task.getGid().equals(gid)) + return task; + } + return null; + } + + public ArrayList getChildTaskList() { + return this.mChildren; + } + + public void setIndex(int index) { + this.mIndex = index; + } + + public int getIndex() { + return this.mIndex; + } +} diff --git a/src/net/micode/notes/gtask/exception/ActionFailureException.java b/src/net/micode/notes/gtask/exception/ActionFailureException.java new file mode 100644 index 0000000..15504be --- /dev/null +++ b/src/net/micode/notes/gtask/exception/ActionFailureException.java @@ -0,0 +1,33 @@ +/* + * 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.gtask.exception; + +public class ActionFailureException extends RuntimeException { + private static final long serialVersionUID = 4425249765923293627L; + + public ActionFailureException() { + super(); + } + + public ActionFailureException(String paramString) { + super(paramString); + } + + public ActionFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} diff --git a/src/net/micode/notes/gtask/exception/NetworkFailureException.java b/src/net/micode/notes/gtask/exception/NetworkFailureException.java new file mode 100644 index 0000000..b08cfb1 --- /dev/null +++ b/src/net/micode/notes/gtask/exception/NetworkFailureException.java @@ -0,0 +1,33 @@ +/* + * 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.gtask.exception; + +public class NetworkFailureException extends Exception { + private static final long serialVersionUID = 2107610287180234136L; + + public NetworkFailureException() { + super(); + } + + public NetworkFailureException(String paramString) { + super(paramString); + } + + public NetworkFailureException(String paramString, Throwable paramThrowable) { + super(paramString, paramThrowable); + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskASyncTask.java b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java new file mode 100644 index 0000000..b3b61e7 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskASyncTask.java @@ -0,0 +1,123 @@ + +/* + * 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.gtask.remote; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; +import net.micode.notes.ui.NotesPreferenceActivity; + + +public class GTaskASyncTask extends AsyncTask { + + private static int GTASK_SYNC_NOTIFICATION_ID = 5234235; + + public interface OnCompleteListener { + void onComplete(); + } + + private Context mContext; + + private NotificationManager mNotifiManager; + + private GTaskManager mTaskManager; + + private OnCompleteListener mOnCompleteListener; + + public GTaskASyncTask(Context context, OnCompleteListener listener) { + mContext = context; + mOnCompleteListener = listener; + mNotifiManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + mTaskManager = GTaskManager.getInstance(); + } + + public void cancelSync() { + mTaskManager.cancelSync(); + } + + public void publishProgess(String message) { + publishProgress(new String[] { + message + }); + } + + private void showNotification(int tickerId, String content) { + Notification notification = new Notification(R.drawable.notification, mContext + .getString(tickerId), System.currentTimeMillis()); + notification.defaults = Notification.DEFAULT_LIGHTS; + notification.flags = Notification.FLAG_AUTO_CANCEL; + PendingIntent pendingIntent; + if (tickerId != R.string.ticker_success) { + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesPreferenceActivity.class), 0); + + } else { + pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext, + NotesListActivity.class), 0); + } + notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content, + pendingIntent); + mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification); + } + + @Override + protected Integer doInBackground(Void... unused) { + publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity + .getSyncAccountName(mContext))); + return mTaskManager.sync(mContext, this); + } + + @Override + protected void onProgressUpdate(String... progress) { + showNotification(R.string.ticker_syncing, progress[0]); + if (mContext instanceof GTaskSyncService) { + ((GTaskSyncService) mContext).sendBroadcast(progress[0]); + } + } + + @Override + protected void onPostExecute(Integer result) { + if (result == GTaskManager.STATE_SUCCESS) { + showNotification(R.string.ticker_success, mContext.getString( + R.string.success_sync_account, mTaskManager.getSyncAccount())); + NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis()); + } else if (result == GTaskManager.STATE_NETWORK_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network)); + } else if (result == GTaskManager.STATE_INTERNAL_ERROR) { + showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal)); + } else if (result == GTaskManager.STATE_SYNC_CANCELLED) { + showNotification(R.string.ticker_cancel, mContext + .getString(R.string.error_sync_cancelled)); + } + if (mOnCompleteListener != null) { + new Thread(new Runnable() { + + public void run() { + mOnCompleteListener.onComplete(); + } + }).start(); + } + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskClient.java b/src/net/micode/notes/gtask/remote/GTaskClient.java new file mode 100644 index 0000000..c67dfdf --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskClient.java @@ -0,0 +1,585 @@ +/* + * 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.gtask.remote; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.GTaskStringUtils; +import net.micode.notes.ui.NotesPreferenceActivity; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + + +public class GTaskClient { + private static final String TAG = GTaskClient.class.getSimpleName(); + + private static final String GTASK_URL = "https://mail.google.com/tasks/"; + + private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig"; + + private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig"; + + private static GTaskClient mInstance = null; + + private DefaultHttpClient mHttpClient; + + private String mGetUrl; + + private String mPostUrl; + + private long mClientVersion; + + private boolean mLoggedin; + + private long mLastLoginTime; + + private int mActionId; + + private Account mAccount; + + private JSONArray mUpdateArray; + + private GTaskClient() { + mHttpClient = null; + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + mClientVersion = -1; + mLoggedin = false; + mLastLoginTime = 0; + mActionId = 1; + mAccount = null; + mUpdateArray = null; + } + + public static synchronized GTaskClient getInstance() { + if (mInstance == null) { + mInstance = new GTaskClient(); + } + return mInstance; + } + + public boolean login(Activity activity) { + // we suppose that the cookie would expire after 5 minutes + // then we need to re-login + final long interval = 1000 * 60 * 5; + if (mLastLoginTime + interval < System.currentTimeMillis()) { + mLoggedin = false; + } + + // need to re-login after account switch + if (mLoggedin + && !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity + .getSyncAccountName(activity))) { + mLoggedin = false; + } + + if (mLoggedin) { + Log.d(TAG, "already logged in"); + return true; + } + + mLastLoginTime = System.currentTimeMillis(); + String authToken = loginGoogleAccount(activity, false); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + // login with custom domain if necessary + if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase() + .endsWith("googlemail.com"))) { + StringBuilder url = new StringBuilder(GTASK_URL).append("a/"); + int index = mAccount.name.indexOf('@') + 1; + String suffix = mAccount.name.substring(index); + url.append(suffix + "/"); + mGetUrl = url.toString() + "ig"; + mPostUrl = url.toString() + "r/ig"; + + if (tryToLoginGtask(activity, authToken)) { + mLoggedin = true; + } + } + + // try to login with google official url + if (!mLoggedin) { + mGetUrl = GTASK_GET_URL; + mPostUrl = GTASK_POST_URL; + if (!tryToLoginGtask(activity, authToken)) { + return false; + } + } + + mLoggedin = true; + return true; + } + + private String loginGoogleAccount(Activity activity, boolean invalidateToken) { + String authToken; + AccountManager accountManager = AccountManager.get(activity); + Account[] accounts = accountManager.getAccountsByType("com.google"); + + if (accounts.length == 0) { + Log.e(TAG, "there is no available google account"); + return null; + } + + String accountName = NotesPreferenceActivity.getSyncAccountName(activity); + Account account = null; + for (Account a : accounts) { + if (a.name.equals(accountName)) { + account = a; + break; + } + } + if (account != null) { + mAccount = account; + } else { + Log.e(TAG, "unable to get an account with the same name in the settings"); + return null; + } + + // get the token now + AccountManagerFuture accountManagerFuture = accountManager.getAuthToken(account, + "goanna_mobile", null, activity, null, null); + try { + Bundle authTokenBundle = accountManagerFuture.getResult(); + authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN); + if (invalidateToken) { + accountManager.invalidateAuthToken("com.google", authToken); + loginGoogleAccount(activity, false); + } + } catch (Exception e) { + Log.e(TAG, "get auth token failed"); + authToken = null; + } + + return authToken; + } + + private boolean tryToLoginGtask(Activity activity, String authToken) { + if (!loginGtask(authToken)) { + // maybe the auth token is out of date, now let's invalidate the + // token and try again + authToken = loginGoogleAccount(activity, true); + if (authToken == null) { + Log.e(TAG, "login google account failed"); + return false; + } + + if (!loginGtask(authToken)) { + Log.e(TAG, "login gtask failed"); + return false; + } + } + return true; + } + + private boolean loginGtask(String authToken) { + int timeoutConnection = 10000; + int timeoutSocket = 15000; + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); + HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); + mHttpClient = new DefaultHttpClient(httpParameters); + BasicCookieStore localBasicCookieStore = new BasicCookieStore(); + mHttpClient.setCookieStore(localBasicCookieStore); + HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false); + + // login gtask + try { + String loginUrl = mGetUrl + "?auth=" + authToken; + HttpGet httpGet = new HttpGet(loginUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the cookie now + List cookies = mHttpClient.getCookieStore().getCookies(); + boolean hasAuthCookie = false; + for (Cookie cookie : cookies) { + if (cookie.getName().contains("GTL")) { + hasAuthCookie = true; + } + } + if (!hasAuthCookie) { + Log.w(TAG, "it seems that there is no auth cookie"); + } + + // get the client version + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + mClientVersion = js.getLong("v"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return false; + } catch (Exception e) { + // simply catch all exceptions + Log.e(TAG, "httpget gtask_url failed"); + return false; + } + + return true; + } + + private int getActionId() { + return mActionId++; + } + + private HttpPost createHttpPost() { + HttpPost httpPost = new HttpPost(mPostUrl); + httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); + httpPost.setHeader("AT", "1"); + return httpPost; + } + + private String getResponseContent(HttpEntity entity) throws IOException { + String contentEncoding = null; + if (entity.getContentEncoding() != null) { + contentEncoding = entity.getContentEncoding().getValue(); + Log.d(TAG, "encoding: " + contentEncoding); + } + + InputStream input = entity.getContent(); + if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) { + input = new GZIPInputStream(entity.getContent()); + } else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) { + Inflater inflater = new Inflater(true); + input = new InflaterInputStream(entity.getContent(), inflater); + } + + try { + InputStreamReader isr = new InputStreamReader(input); + BufferedReader br = new BufferedReader(isr); + StringBuilder sb = new StringBuilder(); + + while (true) { + String buff = br.readLine(); + if (buff == null) { + return sb.toString(); + } + sb = sb.append(buff); + } + } finally { + input.close(); + } + } + + private JSONObject postRequest(JSONObject js) throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + HttpPost httpPost = createHttpPost(); + try { + LinkedList list = new LinkedList(); + list.add(new BasicNameValuePair("r", js.toString())); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8"); + httpPost.setEntity(entity); + + // execute the post + HttpResponse response = mHttpClient.execute(httpPost); + String jsString = getResponseContent(response.getEntity()); + return new JSONObject(jsString); + + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("postRequest failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("unable to convert response content to jsonobject"); + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("error occurs when posting request"); + } + } + + public void createTask(Task task) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + actionList.put(task.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create task: handing jsonobject failed"); + } + } + + public void createTaskList(TaskList tasklist) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + actionList.put(tasklist.getCreateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + // post + JSONObject jsResponse = postRequest(jsPost); + JSONObject jsResult = (JSONObject) jsResponse.getJSONArray( + GTaskStringUtils.GTASK_JSON_RESULTS).get(0); + tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID)); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("create tasklist: handing jsonobject failed"); + } + } + + public void commitUpdate() throws NetworkFailureException { + if (mUpdateArray != null) { + try { + JSONObject jsPost = new JSONObject(); + + // action_list + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + mUpdateArray = null; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("commit update: handing jsonobject failed"); + } + } + } + + public void addUpdateNode(Node node) throws NetworkFailureException { + if (node != null) { + // too many update items may result in an error + // set max to 10 items + if (mUpdateArray != null && mUpdateArray.length() > 10) { + commitUpdate(); + } + + if (mUpdateArray == null) + mUpdateArray = new JSONArray(); + mUpdateArray.put(node.getUpdateAction(getActionId())); + } + } + + public void moveTask(Task task, TaskList preParent, TaskList curParent) + throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); + + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid()); + if (preParent == curParent && task.getPriorSibling() != null) { + // put prioring_sibing_id only if moving within the tasklist and + // it is not the first one + action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling()); + } + action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid()); + action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid()); + if (preParent != curParent) { + // put the dest_list only if moving between tasklists + action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid()); + } + actionList.put(action); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("move task: handing jsonobject failed"); + } + } + + public void deleteNode(Node node) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + + // action_list + node.setDeleted(true); + actionList.put(node.getUpdateAction(getActionId())); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + postRequest(jsPost); + mUpdateArray = null; + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("delete node: handing jsonobject failed"); + } + } + + public JSONArray getTaskLists() throws NetworkFailureException { + if (!mLoggedin) { + Log.e(TAG, "please login first"); + throw new ActionFailureException("not logged in"); + } + + try { + HttpGet httpGet = new HttpGet(mGetUrl); + HttpResponse response = null; + response = mHttpClient.execute(httpGet); + + // get the task list + String resString = getResponseContent(response.getEntity()); + String jsBegin = "_setup("; + String jsEnd = ")}"; + int begin = resString.indexOf(jsBegin); + int end = resString.lastIndexOf(jsEnd); + String jsString = null; + if (begin != -1 && end != -1 && begin < end) { + jsString = resString.substring(begin + jsBegin.length(), end); + } + JSONObject js = new JSONObject(jsString); + return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS); + } catch (ClientProtocolException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (IOException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new NetworkFailureException("gettasklists: httpget failed"); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task lists: handing jasonobject failed"); + } + } + + public JSONArray getTaskList(String listGid) throws NetworkFailureException { + commitUpdate(); + try { + JSONObject jsPost = new JSONObject(); + JSONArray actionList = new JSONArray(); + JSONObject action = new JSONObject(); + + // action_list + action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE, + GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL); + action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId()); + action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid); + action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false); + actionList.put(action); + jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList); + + // client_version + jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion); + + JSONObject jsResponse = postRequest(jsPost); + return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS); + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("get task list: handing jsonobject failed"); + } + } + + public Account getSyncAccount() { + return mAccount; + } + + public void resetUpdateArray() { + mUpdateArray = null; + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskManager.java b/src/net/micode/notes/gtask/remote/GTaskManager.java new file mode 100644 index 0000000..d2b4082 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskManager.java @@ -0,0 +1,800 @@ +/* + * 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.gtask.remote; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +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.NoteColumns; +import net.micode.notes.gtask.data.MetaData; +import net.micode.notes.gtask.data.Node; +import net.micode.notes.gtask.data.SqlNote; +import net.micode.notes.gtask.data.Task; +import net.micode.notes.gtask.data.TaskList; +import net.micode.notes.gtask.exception.ActionFailureException; +import net.micode.notes.gtask.exception.NetworkFailureException; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.GTaskStringUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + + +public class GTaskManager { + private static final String TAG = GTaskManager.class.getSimpleName(); + + public static final int STATE_SUCCESS = 0; + + public static final int STATE_NETWORK_ERROR = 1; + + public static final int STATE_INTERNAL_ERROR = 2; + + public static final int STATE_SYNC_IN_PROGRESS = 3; + + public static final int STATE_SYNC_CANCELLED = 4; + + private static GTaskManager mInstance = null; + + private Activity mActivity; + + private Context mContext; + + private ContentResolver mContentResolver; + + private boolean mSyncing; + + private boolean mCancelled; + + private HashMap mGTaskListHashMap; + + private HashMap mGTaskHashMap; + + private HashMap mMetaHashMap; + + private TaskList mMetaList; + + private HashSet mLocalDeleteIdMap; + + private HashMap mGidToNid; + + private HashMap mNidToGid; + + private GTaskManager() { + mSyncing = false; + mCancelled = false; + mGTaskListHashMap = new HashMap(); + mGTaskHashMap = new HashMap(); + mMetaHashMap = new HashMap(); + mMetaList = null; + mLocalDeleteIdMap = new HashSet(); + mGidToNid = new HashMap(); + mNidToGid = new HashMap(); + } + + public static synchronized GTaskManager getInstance() { + if (mInstance == null) { + mInstance = new GTaskManager(); + } + return mInstance; + } + + public synchronized void setActivityContext(Activity activity) { + // used for getting authtoken + mActivity = activity; + } + + public int sync(Context context, GTaskASyncTask asyncTask) { + if (mSyncing) { + Log.d(TAG, "Sync is in progress"); + return STATE_SYNC_IN_PROGRESS; + } + mContext = context; + mContentResolver = mContext.getContentResolver(); + mSyncing = true; + mCancelled = false; + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + + try { + GTaskClient client = GTaskClient.getInstance(); + client.resetUpdateArray(); + + // login google task + if (!mCancelled) { + if (!client.login(mActivity)) { + throw new NetworkFailureException("login google task failed"); + } + } + + // get the task list from google + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); + initGTaskList(); + + // do content sync work + asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing)); + syncContent(); + } catch (NetworkFailureException e) { + Log.e(TAG, e.toString()); + return STATE_NETWORK_ERROR; + } catch (ActionFailureException e) { + Log.e(TAG, e.toString()); + return STATE_INTERNAL_ERROR; + } catch (Exception e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + return STATE_INTERNAL_ERROR; + } finally { + mGTaskListHashMap.clear(); + mGTaskHashMap.clear(); + mMetaHashMap.clear(); + mLocalDeleteIdMap.clear(); + mGidToNid.clear(); + mNidToGid.clear(); + mSyncing = false; + } + + return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS; + } + + private void initGTaskList() throws NetworkFailureException { + if (mCancelled) + return; + GTaskClient client = GTaskClient.getInstance(); + try { + JSONArray jsTaskLists = client.getTaskLists(); + + // init meta list first + mMetaList = null; + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name + .equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) { + mMetaList = new TaskList(); + mMetaList.setContentByRemoteJSON(object); + + // load meta data + JSONArray jsMetas = client.getTaskList(gid); + for (int j = 0; j < jsMetas.length(); j++) { + object = (JSONObject) jsMetas.getJSONObject(j); + MetaData metaData = new MetaData(); + metaData.setContentByRemoteJSON(object); + if (metaData.isWorthSaving()) { + mMetaList.addChildTask(metaData); + if (metaData.getGid() != null) { + mMetaHashMap.put(metaData.getRelatedGid(), metaData); + } + } + } + } + } + + // create meta list if not existed + if (mMetaList == null) { + mMetaList = new TaskList(); + mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META); + GTaskClient.getInstance().createTaskList(mMetaList); + } + + // init task list + for (int i = 0; i < jsTaskLists.length(); i++) { + JSONObject object = jsTaskLists.getJSONObject(i); + String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); + + if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX) + && !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_META)) { + TaskList tasklist = new TaskList(); + tasklist.setContentByRemoteJSON(object); + mGTaskListHashMap.put(gid, tasklist); + mGTaskHashMap.put(gid, tasklist); + + // load tasks + JSONArray jsTasks = client.getTaskList(gid); + for (int j = 0; j < jsTasks.length(); j++) { + object = (JSONObject) jsTasks.getJSONObject(j); + gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); + Task task = new Task(); + task.setContentByRemoteJSON(object); + if (task.isWorthSaving()) { + task.setMetaInfo(mMetaHashMap.get(gid)); + tasklist.addChildTask(task); + mGTaskHashMap.put(gid, task); + } + } + } + } + } catch (JSONException e) { + Log.e(TAG, e.toString()); + e.printStackTrace(); + throw new ActionFailureException("initGTaskList: handing JSONObject failed"); + } + } + + private void syncContent() throws NetworkFailureException { + int syncType; + Cursor c = null; + String gid; + Node node; + + mLocalDeleteIdMap.clear(); + + if (mCancelled) { + return; + } + + // for local deleted note + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id=?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, null); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c); + } + + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + } + } else { + Log.w(TAG, "failed to query trash folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // sync folder first + syncFolder(); + + // for note existing in database + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // remote delete + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing note in database"); + } + + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // go through remaining items + Iterator> iter = mGTaskHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + node = entry.getValue(); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + + // mCancelled can be set by another thread, so we neet to check one by + // one + // clear local delete table + if (!mCancelled) { + if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) { + throw new ActionFailureException("failed to batch-delete local deleted notes"); + } + } + + // refresh local sync id + if (!mCancelled) { + GTaskClient.getInstance().commitUpdate(); + refreshLocalSyncId(); + } + + } + + private void syncFolder() throws NetworkFailureException { + Cursor c = null; + String gid; + Node node; + int syncType; + + if (mCancelled) { + return; + } + + // for root folder + try { + c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null); + if (c != null) { + c.moveToNext(); + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER); + mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid); + // for system folder, only update remote name if necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } else { + Log.w(TAG, "failed to query root folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for call-note folder + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)", + new String[] { + String.valueOf(Notes.ID_CALL_RECORD_FOLDER) + }, null); + if (c != null) { + if (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER); + mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid); + // for system folder, only update remote name if + // necessary + if (!node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + + GTaskStringUtils.FOLDER_CALL_NOTE)) + doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c); + } else { + doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c); + } + } + } else { + Log.w(TAG, "failed to query call note folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for local existing folders + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type=? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + gid = c.getString(SqlNote.GTASK_ID_COLUMN); + node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); + mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); + syncType = node.getSyncAction(c); + } else { + if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) { + // local add + syncType = Node.SYNC_ACTION_ADD_REMOTE; + } else { + // remote delete + syncType = Node.SYNC_ACTION_DEL_LOCAL; + } + } + doContentSync(syncType, node, c); + } + } else { + Log.w(TAG, "failed to query existing folder"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + + // for remote add folders + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + gid = entry.getKey(); + node = entry.getValue(); + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); + } + } + + if (!mCancelled) + GTaskClient.getInstance().commitUpdate(); + } + + private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + MetaData meta; + switch (syncType) { + case Node.SYNC_ACTION_ADD_LOCAL: + addLocalNode(node); + break; + case Node.SYNC_ACTION_ADD_REMOTE: + addRemoteNode(node, c); + break; + case Node.SYNC_ACTION_DEL_LOCAL: + meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN)); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN)); + break; + case Node.SYNC_ACTION_DEL_REMOTE: + meta = mMetaHashMap.get(node.getGid()); + if (meta != null) { + GTaskClient.getInstance().deleteNode(meta); + } + GTaskClient.getInstance().deleteNode(node); + break; + case Node.SYNC_ACTION_UPDATE_LOCAL: + updateLocalNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_REMOTE: + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_UPDATE_CONFLICT: + // merging both modifications maybe a good idea + // right now just use local update simply + updateRemoteNode(node, c); + break; + case Node.SYNC_ACTION_NONE: + break; + case Node.SYNC_ACTION_ERROR: + default: + throw new ActionFailureException("unkown sync action type"); + } + } + + private void addLocalNode(Node node) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + if (node instanceof TaskList) { + if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) { + sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER); + } else if (node.getName().equals( + GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) { + sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER); + } else { + sqlNote = new SqlNote(mContext); + sqlNote.setContent(node.getLocalJSONFromContent()); + sqlNote.setParentId(Notes.ID_ROOT_FOLDER); + } + } else { + sqlNote = new SqlNote(mContext); + JSONObject js = node.getLocalJSONFromContent(); + try { + if (js.has(GTaskStringUtils.META_HEAD_NOTE)) { + JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE); + if (note.has(NoteColumns.ID)) { + long id = note.getLong(NoteColumns.ID); + if (DataUtils.existInNoteDatabase(mContentResolver, id)) { + // the id is not available, have to create a new one + note.remove(NoteColumns.ID); + } + } + } + + if (js.has(GTaskStringUtils.META_HEAD_DATA)) { + JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA); + for (int i = 0; i < dataArray.length(); i++) { + JSONObject data = dataArray.getJSONObject(i); + if (data.has(DataColumns.ID)) { + long dataId = data.getLong(DataColumns.ID); + if (DataUtils.existInDataDatabase(mContentResolver, dataId)) { + // the data id is not available, have to create + // a new one + data.remove(DataColumns.ID); + } + } + } + + } + } catch (JSONException e) { + Log.w(TAG, e.toString()); + e.printStackTrace(); + } + sqlNote.setContent(js); + + Long parentId = mGidToNid.get(((Task) node).getParent().getGid()); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot add local node"); + } + sqlNote.setParentId(parentId.longValue()); + } + + // create the local node + sqlNote.setGtaskId(node.getGid()); + sqlNote.commit(false); + + // update gid-nid mapping + mGidToNid.put(node.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), node.getGid()); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote); + } + + private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote; + // update the note locally + sqlNote = new SqlNote(mContext, c); + sqlNote.setContent(node.getLocalJSONFromContent()); + + Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid()) + : new Long(Notes.ID_ROOT_FOLDER); + if (parentId == null) { + Log.e(TAG, "cannot find task's parent id locally"); + throw new ActionFailureException("cannot update local node"); + } + sqlNote.setParentId(parentId.longValue()); + sqlNote.commit(true); + + // update meta info + updateRemoteMeta(node.getGid(), sqlNote); + } + + private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + Node n; + + // update remotely + if (sqlNote.isNoteType()) { + Task task = new Task(); + task.setContentByLocalJSON(sqlNote.getContent()); + + String parentGid = mNidToGid.get(sqlNote.getParentId()); + if (parentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot add remote task"); + } + mGTaskListHashMap.get(parentGid).addChildTask(task); + + GTaskClient.getInstance().createTask(task); + n = (Node) task; + + // add meta + updateRemoteMeta(task.getGid(), sqlNote); + } else { + TaskList tasklist = null; + + // we need to skip folder if it has already existed + String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX; + if (sqlNote.getId() == Notes.ID_ROOT_FOLDER) + folderName += GTaskStringUtils.FOLDER_DEFAULT; + else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER) + folderName += GTaskStringUtils.FOLDER_CALL_NOTE; + else + folderName += sqlNote.getSnippet(); + + Iterator> iter = mGTaskListHashMap.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + String gid = entry.getKey(); + TaskList list = entry.getValue(); + + if (list.getName().equals(folderName)) { + tasklist = list; + if (mGTaskHashMap.containsKey(gid)) { + mGTaskHashMap.remove(gid); + } + break; + } + } + + // no match we can add now + if (tasklist == null) { + tasklist = new TaskList(); + tasklist.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().createTaskList(tasklist); + mGTaskListHashMap.put(tasklist.getGid(), tasklist); + } + n = (Node) tasklist; + } + + // update local note + sqlNote.setGtaskId(n.getGid()); + sqlNote.commit(false); + sqlNote.resetLocalModified(); + sqlNote.commit(true); + + // gid-id mapping + mGidToNid.put(n.getGid(), sqlNote.getId()); + mNidToGid.put(sqlNote.getId(), n.getGid()); + } + + private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException { + if (mCancelled) { + return; + } + + SqlNote sqlNote = new SqlNote(mContext, c); + + // update remotely + node.setContentByLocalJSON(sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(node); + + // update meta + updateRemoteMeta(node.getGid(), sqlNote); + + // move task if necessary + if (sqlNote.isNoteType()) { + Task task = (Task) node; + TaskList preParentList = task.getParent(); + + String curParentGid = mNidToGid.get(sqlNote.getParentId()); + if (curParentGid == null) { + Log.e(TAG, "cannot find task's parent tasklist"); + throw new ActionFailureException("cannot update remote task"); + } + TaskList curParentList = mGTaskListHashMap.get(curParentGid); + + if (preParentList != curParentList) { + preParentList.removeChildTask(task); + curParentList.addChildTask(task); + GTaskClient.getInstance().moveTask(task, preParentList, curParentList); + } + } + + // clear local modified flag + sqlNote.resetLocalModified(); + sqlNote.commit(true); + } + + private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException { + if (sqlNote != null && sqlNote.isNoteType()) { + MetaData metaData = mMetaHashMap.get(gid); + if (metaData != null) { + metaData.setMeta(gid, sqlNote.getContent()); + GTaskClient.getInstance().addUpdateNode(metaData); + } else { + metaData = new MetaData(); + metaData.setMeta(gid, sqlNote.getContent()); + mMetaList.addChildTask(metaData); + mMetaHashMap.put(gid, metaData); + GTaskClient.getInstance().createTask(metaData); + } + } + } + + private void refreshLocalSyncId() throws NetworkFailureException { + if (mCancelled) { + return; + } + + // get the latest gtask list + mGTaskHashMap.clear(); + mGTaskListHashMap.clear(); + mMetaHashMap.clear(); + initGTaskList(); + + Cursor c = null; + try { + c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, + "(type<>? AND parent_id<>?)", new String[] { + String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER) + }, NoteColumns.TYPE + " DESC"); + if (c != null) { + while (c.moveToNext()) { + String gid = c.getString(SqlNote.GTASK_ID_COLUMN); + Node node = mGTaskHashMap.get(gid); + if (node != null) { + mGTaskHashMap.remove(gid); + ContentValues values = new ContentValues(); + values.put(NoteColumns.SYNC_ID, node.getLastModified()); + mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, + c.getLong(SqlNote.ID_COLUMN)), values, null, null); + } else { + Log.e(TAG, "something is missed"); + throw new ActionFailureException( + "some local items don't have gid after sync"); + } + } + } else { + Log.w(TAG, "failed to query local note to refresh sync id"); + } + } finally { + if (c != null) { + c.close(); + c = null; + } + } + } + + public String getSyncAccount() { + return GTaskClient.getInstance().getSyncAccount().name; + } + + public void cancelSync() { + mCancelled = true; + } +} diff --git a/src/net/micode/notes/gtask/remote/GTaskSyncService.java b/src/net/micode/notes/gtask/remote/GTaskSyncService.java new file mode 100644 index 0000000..cca36f7 --- /dev/null +++ b/src/net/micode/notes/gtask/remote/GTaskSyncService.java @@ -0,0 +1,128 @@ +/* + * 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.gtask.remote; + +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class GTaskSyncService extends Service { + public final static String ACTION_STRING_NAME = "sync_action_type"; + + public final static int ACTION_START_SYNC = 0; + + public final static int ACTION_CANCEL_SYNC = 1; + + public final static int ACTION_INVALID = 2; + + public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service"; + + public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing"; + + public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg"; + + private static GTaskASyncTask mSyncTask = null; + + private static String mSyncProgress = ""; + + private void startSync() { + if (mSyncTask == null) { + mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() { + public void onComplete() { + mSyncTask = null; + sendBroadcast(""); + stopSelf(); + } + }); + sendBroadcast(""); + mSyncTask.execute(); + } + } + + private void cancelSync() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + @Override + public void onCreate() { + mSyncTask = null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Bundle bundle = intent.getExtras(); + if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) { + switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) { + case ACTION_START_SYNC: + startSync(); + break; + case ACTION_CANCEL_SYNC: + cancelSync(); + break; + default: + break; + } + return START_STICKY; + } + return super.onStartCommand(intent, flags, startId); + } + + @Override + public void onLowMemory() { + if (mSyncTask != null) { + mSyncTask.cancelSync(); + } + } + + public IBinder onBind(Intent intent) { + return null; + } + + public void sendBroadcast(String msg) { + mSyncProgress = msg; + Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); + intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null); + intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg); + sendBroadcast(intent); + } + + public static void startSync(Activity activity) { + GTaskManager.getInstance().setActivityContext(activity); + Intent intent = new Intent(activity, GTaskSyncService.class); + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC); + activity.startService(intent); + } + + public static void cancelSync(Context context) { + Intent intent = new Intent(context, GTaskSyncService.class); + intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC); + context.startService(intent); + } + + public static boolean isSyncing() { + return mSyncTask != null; + } + + public static String getProgressString() { + return mSyncProgress; + } +} diff --git a/src/net/micode/notes/model/Note.java b/src/net/micode/notes/model/Note.java new file mode 100644 index 0000000..6706cf6 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/model/WorkingNote.java b/src/net/micode/notes/model/WorkingNote.java new file mode 100644 index 0000000..be081e4 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/tool/BackupUtils.java b/src/net/micode/notes/tool/BackupUtils.java new file mode 100644 index 0000000..39f6ec4 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/tool/DataUtils.java b/src/net/micode/notes/tool/DataUtils.java new file mode 100644 index 0000000..2a14982 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/tool/GTaskStringUtils.java b/src/net/micode/notes/tool/GTaskStringUtils.java new file mode 100644 index 0000000..666b729 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/tool/ResourceParser.java b/src/net/micode/notes/tool/ResourceParser.java new file mode 100644 index 0000000..1ad3ad6 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/ui/AlarmAlertActivity.java b/src/net/micode/notes/ui/AlarmAlertActivity.java new file mode 100644 index 0000000..85723be --- /dev/null +++ b/src/net/micode/notes/ui/AlarmAlertActivity.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.Window; +import android.view.WindowManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.DataUtils; + +import java.io.IOException; + + +public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener { + private long mNoteId; + private String mSnippet; + private static final int SNIPPET_PREW_MAX_LEN = 60; + MediaPlayer mPlayer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + final Window win = getWindow(); + win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + + if (!isScreenOn()) { + win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + } + + Intent intent = getIntent(); + + try { + mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1)); + mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId); + mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0, + SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info) + : mSnippet; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return; + } + + mPlayer = new MediaPlayer(); + if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) { + showActionDialog(); + playAlarmSound(); + } else { + finish(); + } + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + private void playAlarmSound() { + Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM); + + int silentModeStreams = Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + + if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) { + mPlayer.setAudioStreamType(silentModeStreams); + } else { + mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + } + try { + mPlayer.setDataSource(this, url); + mPlayer.prepare(); + mPlayer.setLooping(true); + mPlayer.start(); + } catch (IllegalArgumentException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (SecurityException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void showActionDialog() { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.setTitle(R.string.app_name); + dialog.setMessage(mSnippet); + dialog.setPositiveButton(R.string.notealert_ok, this); + if (isScreenOn()) { + dialog.setNegativeButton(R.string.notealert_enter, this); + } + dialog.show().setOnDismissListener(this); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_NEGATIVE: + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, mNoteId); + startActivity(intent); + break; + default: + break; + } + } + + public void onDismiss(DialogInterface dialog) { + stopAlarmSound(); + finish(); + } + + private void stopAlarmSound() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + } + } +} diff --git a/src/net/micode/notes/ui/AlarmInitReceiver.java b/src/net/micode/notes/ui/AlarmInitReceiver.java new file mode 100644 index 0000000..f221202 --- /dev/null +++ b/src/net/micode/notes/ui/AlarmInitReceiver.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.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class AlarmInitReceiver extends BroadcastReceiver { + + private static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE + }; + + private static final int COLUMN_ID = 0; + private static final int COLUMN_ALERTED_DATE = 1; + + @Override + public void onReceive(Context context, Intent intent) { + long currentDate = System.currentTimeMillis(); + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, + new String[] { String.valueOf(currentDate) }, + null); + + if (c != null) { + if (c.moveToFirst()) { + do { + long alertDate = c.getLong(COLUMN_ALERTED_DATE); + Intent sender = new Intent(context, AlarmReceiver.class); + sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID))); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0); + AlarmManager alermManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent); + } while (c.moveToNext()); + } + c.close(); + } + } +} diff --git a/src/net/micode/notes/ui/AlarmReceiver.java b/src/net/micode/notes/ui/AlarmReceiver.java new file mode 100644 index 0000000..54e503b --- /dev/null +++ b/src/net/micode/notes/ui/AlarmReceiver.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AlarmReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + intent.setClass(context, AlarmAlertActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} diff --git a/src/net/micode/notes/ui/DateTimePicker.java b/src/net/micode/notes/ui/DateTimePicker.java new file mode 100644 index 0000000..496b0cd --- /dev/null +++ b/src/net/micode/notes/ui/DateTimePicker.java @@ -0,0 +1,485 @@ +/* + * 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 { + + 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; + private 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(); + } + }; + + private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + boolean isDateChanged = false; + Calendar cal = Calendar.getInstance(); + if (!mIs24HourView) { + if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY || + oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } else { + if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, 1); + isDateChanged = true; + } else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) { + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -1); + isDateChanged = true; + } + } + int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY); + mDate.set(Calendar.HOUR_OF_DAY, newHour); + onDateTimeChanged(); + if (isDateChanged) { + setCurrentYear(cal.get(Calendar.YEAR)); + setCurrentMonth(cal.get(Calendar.MONTH)); + setCurrentDay(cal.get(Calendar.DAY_OF_MONTH)); + } + } + }; + + private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + int offset = 0; + if (oldVal == maxValue && newVal == minValue) { + offset += 1; + } else if (oldVal == minValue && newVal == maxValue) { + offset -= 1; + } + if (offset != 0) { + mDate.add(Calendar.HOUR_OF_DAY, offset); + mHourSpinner.setValue(getCurrentHour()); + updateDateControl(); + int newHour = getCurrentHourOfDay(); + if (newHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + updateAmPmControl(); + } else { + mIsAm = true; + updateAmPmControl(); + } + } + mDate.set(Calendar.MINUTE, newVal); + onDateTimeChanged(); + } + }; + + private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + mIsAm = !mIsAm; + if (mIsAm) { + mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); + } else { + mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); + } + updateAmPmControl(); + onDateTimeChanged(); + } + }; + + public interface OnDateTimeChangedListener { + void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute); + } + + public DateTimePicker(Context context) { + this(context, System.currentTimeMillis()); + } + + public DateTimePicker(Context context, long date) { + this(context, date, DateFormat.is24HourFormat(context)); + } + + public DateTimePicker(Context context, long date, boolean is24HourView) { + super(context); + mDate = Calendar.getInstance(); + mInitialising = true; + mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY; + inflate(context, R.layout.datetime_picker, this); + + mDateSpinner = (NumberPicker) findViewById(R.id.date); + mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL); + mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL); + mDateSpinner.setOnValueChangedListener(mOnDateChangedListener); + + mHourSpinner = (NumberPicker) findViewById(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)); + } + + /** + * 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 + */ + 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) { + if (mIs24HourView == is24HourView) { + return; + } + mIs24HourView = is24HourView; + mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); + int hour = getCurrentHourOfDay(); + updateHourControl(); + setCurrentHour(hour); + updateAmPmControl(); + } + + private void updateDateControl() { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(mDate.getTimeInMillis()); + cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); + mDateSpinner.setDisplayedValues(null); + for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { + cal.add(Calendar.DAY_OF_YEAR, 1); + mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); + } + mDateSpinner.setDisplayedValues(mDateDisplayValues); + mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); + mDateSpinner.invalidate(); + } + + private void updateAmPmControl() { + if (mIs24HourView) { + mAmPmSpinner.setVisibility(View.GONE); + } 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/src/net/micode/notes/ui/DateTimePickerDialog.java b/src/net/micode/notes/ui/DateTimePickerDialog.java new file mode 100644 index 0000000..2c47ba4 --- /dev/null +++ b/src/net/micode/notes/ui/DateTimePickerDialog.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import java.util.Calendar; + +import net.micode.notes.R; +import net.micode.notes.ui.DateTimePicker; +import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.text.format.DateFormat; +import android.text.format.DateUtils; + +public class DateTimePickerDialog extends AlertDialog implements OnClickListener { + + private Calendar mDate = Calendar.getInstance(); + private boolean mIs24HourView; + private OnDateTimeSetListener mOnDateTimeSetListener; + private DateTimePicker mDateTimePicker; + + public interface OnDateTimeSetListener { + void OnDateTimeSet(AlertDialog dialog, long date); + } + + public DateTimePickerDialog(Context context, long date) { + super(context); + mDateTimePicker = new DateTimePicker(context); + setView(mDateTimePicker); + mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() { + public void onDateTimeChanged(DateTimePicker view, int year, int month, + int dayOfMonth, int hourOfDay, int minute) { + mDate.set(Calendar.YEAR, year); + mDate.set(Calendar.MONTH, month); + mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth); + mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); + mDate.set(Calendar.MINUTE, minute); + updateTitle(mDate.getTimeInMillis()); + } + }); + mDate.setTimeInMillis(date); + mDate.set(Calendar.SECOND, 0); + mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); + setButton(context.getString(R.string.datetime_dialog_ok), this); + setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); + set24HourView(DateFormat.is24HourFormat(this.getContext())); + updateTitle(mDate.getTimeInMillis()); + } + + public void set24HourView(boolean is24HourView) { + mIs24HourView = is24HourView; + } + + public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) { + mOnDateTimeSetListener = callBack; + } + + private void updateTitle(long date) { + int flag = + DateUtils.FORMAT_SHOW_YEAR | + DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_SHOW_TIME; + flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR; + setTitle(DateUtils.formatDateTime(this.getContext(), date, flag)); + } + + public void onClick(DialogInterface arg0, int arg1) { + if (mOnDateTimeSetListener != null) { + mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis()); + } + } + +} \ No newline at end of file diff --git a/src/net/micode/notes/ui/DropdownMenu.java b/src/net/micode/notes/ui/DropdownMenu.java new file mode 100644 index 0000000..613dc74 --- /dev/null +++ b/src/net/micode/notes/ui/DropdownMenu.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; + +import net.micode.notes.R; + +public class DropdownMenu { + private Button mButton; + private PopupMenu mPopupMenu; + private Menu mMenu; + + public DropdownMenu(Context context, Button button, int menuId) { + mButton = button; + mButton.setBackgroundResource(R.drawable.dropdown_icon); + mPopupMenu = new PopupMenu(context, mButton); + mMenu = mPopupMenu.getMenu(); + mPopupMenu.getMenuInflater().inflate(menuId, mMenu); + mButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + mPopupMenu.show(); + } + }); + } + + public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) { + if (mPopupMenu != null) { + mPopupMenu.setOnMenuItemClickListener(listener); + } + } + + public MenuItem findItem(int id) { + return mMenu.findItem(id); + } + + public void setTitle(CharSequence title) { + mButton.setText(title); + } +} diff --git a/src/net/micode/notes/ui/FoldersListAdapter.java b/src/net/micode/notes/ui/FoldersListAdapter.java new file mode 100644 index 0000000..96b77da --- /dev/null +++ b/src/net/micode/notes/ui/FoldersListAdapter.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; + + +public class FoldersListAdapter extends CursorAdapter { + public static final String [] PROJECTION = { + NoteColumns.ID, + NoteColumns.SNIPPET + }; + + public static final int ID_COLUMN = 0; + public static final int NAME_COLUMN = 1; + + public FoldersListAdapter(Context context, Cursor c) { + super(context, c); + // TODO Auto-generated constructor stub + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new FolderListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof FolderListItem) { + String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + ((FolderListItem) view).bind(folderName); + } + } + + public String getFolderName(Context context, int position) { + Cursor cursor = (Cursor) getItem(position); + return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context + .getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN); + } + + private class FolderListItem extends LinearLayout { + private TextView mName; + + public FolderListItem(Context context) { + super(context); + inflate(context, R.layout.folder_list_item, this); + mName = (TextView) findViewById(R.id.tv_folder_name); + } + + public void bind(String name) { + mName.setText(name); + } + } + +} diff --git a/src/net/micode/notes/ui/NoteEditActivity.java b/src/net/micode/notes/ui/NoteEditActivity.java new file mode 100644 index 0000000..96a9ff8 --- /dev/null +++ b/src/net/micode/notes/ui/NoteEditActivity.java @@ -0,0 +1,873 @@ +/* + * 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; + } + + 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); + } + + 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); + } + + 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); + } + + private static final Map sFontSelectorSelectionMap = new HashMap(); + static { + sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select); + sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select); + } + + private static final String TAG = "NoteEditActivity"; + + private HeadViewHolder mNoteHeaderHolder; + + private View mHeadViewPanel; + + private View mNoteBgColorSelector; + + private View mFontSizeSelector; + + private EditText mNoteEditor; + + private View mNoteEditorPanel; + + private WorkingNote mWorkingNote; + + private SharedPreferences mSharedPrefs; + private int mFontSizeId; + + 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 = ""; + + /** + * Starting from the searched result + */ + if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) { + noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY)); + mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY); + } + + if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) { + Intent jump = new Intent(this, NotesListActivity.class); + startActivity(jump); + showToast(R.string.error_note_not_exist); + finish(); + return false; + } else { + mWorkingNote = WorkingNote.load(this, noteId); + if (mWorkingNote == null) { + Log.e(TAG, "load note failed with note id" + noteId); + finish(); + return false; + } + } + getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + } else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) { + // New note + 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)); + + // 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); + } + + 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 + public boolean dispatchTouchEvent(MotionEvent ev) { + if (mNoteBgColorSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mNoteBgColorSelector, ev)) { + mNoteBgColorSelector.setVisibility(View.GONE); + return true; + } + + if (mFontSizeSelector.getVisibility() == View.VISIBLE + && !inRangeOfView(mFontSizeSelector, ev)) { + mFontSizeSelector.setVisibility(View.GONE); + return true; + } + return super.dispatchTouchEvent(ev); + } + + private boolean inRangeOfView(View view, MotionEvent ev) { + int []location = new int[2]; + view.getLocationOnScreen(location); + int x = location[0]; + int y = location[1]; + if (ev.getX() < x + || ev.getX() > (x + view.getWidth()) + || ev.getY() < y + || ev.getY() > (y + view.getHeight())) { + return false; + } + return true; + } + + private void initResources() { + mHeadViewPanel = findViewById(R.id.note_title); + mNoteHeaderHolder = new HeadViewHolder(); + mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date); + mNoteHeaderHolder.ivAlertIcon = (ImageView) findViewById(R.id.iv_alert_icon); + mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date); + mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color); + mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this); + mNoteEditor = (EditText) findViewById(R.id.note_edit_view); + mNoteEditorPanel = findViewById(R.id.sv_note_edit); + mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector); + for (int id : sBgSelectorBtnsMap.keySet()) { + ImageView iv = (ImageView) findViewById(id); + iv.setOnClickListener(this); + } + + mFontSizeSelector = findViewById(R.id.font_size_selector); + for (int id : sFontSizeBtnsMap.keySet()) { + View view = findViewById(id); + view.setOnClickListener(this); + }; + mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + mFontSizeId = mSharedPrefs.getInt(PREFERENCE_FONT_SIZE, ResourceParser.BG_DEFAULT_FONT_SIZE); + /** + * 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); + } 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()) { + case R.id.menu_new_note: + createNewNote(); + break; + case R.id.menu_delete: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_note)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteCurrentNote(); + finish(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.menu_font_size: + mFontSizeSelector.setVisibility(View.VISIBLE); + findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE); + break; + case R.id.menu_list_mode: + mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ? + TextNote.MODE_CHECK_LIST : 0); + break; + case R.id.menu_share: + getWorkingText(); + sendTo(this, mWorkingNote.getContent()); + break; + case R.id.menu_send_to_desktop: + sendToDesktop(); + break; + case R.id.menu_alert: + setReminder(); + break; + case R.id.menu_delete_remind: + mWorkingNote.setAlertDate(0, false); + break; + default: + break; + } + 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.putExtra(Intent.EXTRA_TEXT, info); + intent.setType("text/plain"); + context.startActivity(intent); + } + + 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()); + startActivity(intent); + } + + private void deleteCurrentNote() { + if (mWorkingNote.existInDatabase()) { + HashSet ids = new HashSet(); + long id = mWorkingNote.getNoteId(); + if (id != Notes.ID_ROOT_FOLDER) { + ids.add(id); + } else { + Log.d(TAG, "Wrong note id, should not happen"); + } + if (!isSyncMode()) { + if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) { + Log.e(TAG, "Delete Note error"); + } + } else { + if (!DataUtils.batchMoveToFolder(getContentResolver(), ids, Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + } + mWorkingNote.markDeleted(true); + } + + private boolean isSyncMode() { + 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())); + 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); + } + } + + public void onWidgetChanged() { + updateWidget(); + } + + public void onEditTextDelete(int index, String text) { + int childCount = mEditTextList.getChildCount(); + if (childCount == 1) { + return; + } + + for (int i = index + 1; i < childCount; i++) { + ((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text)) + .setIndex(i - 1); + } + + mEditTextList.removeViewAt(index); + NoteEditText edit = null; + if(index == 0) { + edit = (NoteEditText) mEditTextList.getChildAt(0).findViewById( + R.id.et_edit_text); + } else { + edit = (NoteEditText) mEditTextList.getChildAt(index - 1).findViewById( + R.id.et_edit_text); + } + int length = edit.length(); + edit.append(text); + edit.requestFocus(); + edit.setSelection(length); + } + + public void onEditTextEnter(int index, String text) { + /** + * 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); + 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; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + hasChecked = true; + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mWorkingNote.setWorkingText(sb.toString()); + } else { + mWorkingNote.setWorkingText(mNoteEditor.getText().toString()); + } + return hasChecked; + } + + private boolean saveNote() { + getWorkingText(); + boolean saved = mWorkingNote.saveNote(); + if (saved) { + /** + * 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 + */ + 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"); + 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; + } + + private void showToast(int resId) { + showToast(resId, Toast.LENGTH_SHORT); + } + + private void showToast(int resId, int duration) { + Toast.makeText(this, resId, duration).show(); + } +} diff --git a/src/net/micode/notes/ui/NoteEditText.java b/src/net/micode/notes/ui/NoteEditText.java new file mode 100644 index 0000000..2afe2a8 --- /dev/null +++ b/src/net/micode/notes/ui/NoteEditText.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.widget.EditText; + +import net.micode.notes.R; + +import java.util.HashMap; +import java.util.Map; + +public class NoteEditText extends EditText { + private static final String TAG = "NoteEditText"; + private int mIndex; + private int mSelectionStartBeforeDelete; + + private static final String SCHEME_TEL = "tel:" ; + private static final String SCHEME_HTTP = "http:" ; + private static final String SCHEME_EMAIL = "mailto:" ; + + private static final Map sSchemaActionResMap = new HashMap(); + static { + sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); + sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); + sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + } + + /** + * Call by the {@link NoteEditActivity} to delete or add edit text + */ + public interface OnTextViewChangeListener { + /** + * Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens + * and the text is null + */ + void onEditTextDelete(int index, String text); + + /** + * Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER} + * happen + */ + void onEditTextEnter(int index, String text); + + /** + * Hide or show item option when text change + */ + void onTextChange(int index, boolean hasText); + } + + private OnTextViewChangeListener mOnTextViewChangeListener; + + public NoteEditText(Context context) { + super(context, null); + mIndex = 0; + } + + public void setIndex(int index) { + mIndex = index; + } + + public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { + mOnTextViewChangeListener = listener; + } + + public NoteEditText(Context context, AttributeSet attrs) { + super(context, attrs, android.R.attr.editTextStyle); + } + + public NoteEditText(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // TODO Auto-generated constructor stub + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + + int x = (int) event.getX(); + int y = (int) event.getY(); + x -= getTotalPaddingLeft(); + y -= getTotalPaddingTop(); + x += getScrollX(); + y += getScrollY(); + + Layout layout = getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + Selection.setSelection(getText(), off); + break; + } + + return super.onTouchEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + return false; + } + break; + case KeyEvent.KEYCODE_DEL: + mSelectionStartBeforeDelete = getSelectionStart(); + break; + default: + break; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + switch(keyCode) { + case KeyEvent.KEYCODE_DEL: + if (mOnTextViewChangeListener != null) { + if (0 == mSelectionStartBeforeDelete && mIndex != 0) { + mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); + return true; + } + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + case KeyEvent.KEYCODE_ENTER: + if (mOnTextViewChangeListener != null) { + int selectionStart = getSelectionStart(); + String text = getText().subSequence(selectionStart, length()).toString(); + setText(getText().subSequence(0, selectionStart)); + mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); + } else { + Log.d(TAG, "OnTextViewChangeListener was not seted"); + } + break; + default: + break; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (mOnTextViewChangeListener != null) { + if (!focused && TextUtils.isEmpty(getText())) { + mOnTextViewChangeListener.onTextChange(mIndex, false); + } else { + mOnTextViewChangeListener.onTextChange(mIndex, true); + } + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + @Override + protected void onCreateContextMenu(ContextMenu menu) { + if (getText() instanceof Spanned) { + int selStart = getSelectionStart(); + int selEnd = getSelectionEnd(); + + int min = Math.min(selStart, selEnd); + int max = Math.max(selStart, selEnd); + + final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); + if (urls.length == 1) { + int defaultResId = 0; + for(String schema: sSchemaActionResMap.keySet()) { + if(urls[0].getURL().indexOf(schema) >= 0) { + defaultResId = sSchemaActionResMap.get(schema); + break; + } + } + + if (defaultResId == 0) { + defaultResId = R.string.note_link_other; + } + + menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( + new OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + // goto a new intent + urls[0].onClick(NoteEditText.this); + return true; + } + }); + } + } + super.onCreateContextMenu(menu); + } +} diff --git a/src/net/micode/notes/ui/NoteItemData.java b/src/net/micode/notes/ui/NoteItemData.java new file mode 100644 index 0000000..0f5a878 --- /dev/null +++ b/src/net/micode/notes/ui/NoteItemData.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; + +import net.micode.notes.data.Contact; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.DataUtils; + + +public class NoteItemData { + static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.ALERTED_DATE, + NoteColumns.BG_COLOR_ID, + NoteColumns.CREATED_DATE, + NoteColumns.HAS_ATTACHMENT, + NoteColumns.MODIFIED_DATE, + NoteColumns.NOTES_COUNT, + NoteColumns.PARENT_ID, + NoteColumns.SNIPPET, + NoteColumns.TYPE, + NoteColumns.WIDGET_ID, + NoteColumns.WIDGET_TYPE, + }; + + private static final int ID_COLUMN = 0; + private static final int ALERTED_DATE_COLUMN = 1; + private static final int BG_COLOR_ID_COLUMN = 2; + private static final int CREATED_DATE_COLUMN = 3; + private static final int HAS_ATTACHMENT_COLUMN = 4; + private static final int MODIFIED_DATE_COLUMN = 5; + private static final int NOTES_COUNT_COLUMN = 6; + private static final int PARENT_ID_COLUMN = 7; + private static final int SNIPPET_COLUMN = 8; + private static final int TYPE_COLUMN = 9; + private static final int WIDGET_ID_COLUMN = 10; + private static final int WIDGET_TYPE_COLUMN = 11; + + private long mId; + private long mAlertDate; + private int mBgColorId; + private long mCreatedDate; + private boolean mHasAttachment; + private long mModifiedDate; + private int mNotesCount; + private long mParentId; + private String mSnippet; + private int mType; + private int mWidgetId; + private int mWidgetType; + private String mName; + private String mPhoneNumber; + + private boolean mIsLastItem; + private boolean mIsFirstItem; + private boolean mIsOnlyOneItem; + private boolean mIsOneNoteFollowingFolder; + private boolean mIsMultiNotesFollowingFolder; + + public NoteItemData(Context context, Cursor cursor) { + mId = cursor.getLong(ID_COLUMN); + mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN); + mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN); + mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN); + mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false; + mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN); + mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN); + mParentId = cursor.getLong(PARENT_ID_COLUMN); + mSnippet = cursor.getString(SNIPPET_COLUMN); + mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace( + NoteEditActivity.TAG_UNCHECKED, ""); + mType = cursor.getInt(TYPE_COLUMN); + mWidgetId = cursor.getInt(WIDGET_ID_COLUMN); + mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN); + + mPhoneNumber = ""; + if (mParentId == Notes.ID_CALL_RECORD_FOLDER) { + mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId); + if (!TextUtils.isEmpty(mPhoneNumber)) { + mName = Contact.getContact(context, mPhoneNumber); + if (mName == null) { + mName = mPhoneNumber; + } + } + } + + if (mName == null) { + mName = ""; + } + checkPostion(cursor); + } + + private void checkPostion(Cursor cursor) { + mIsLastItem = cursor.isLast() ? true : false; + mIsFirstItem = cursor.isFirst() ? true : false; + mIsOnlyOneItem = (cursor.getCount() == 1); + mIsMultiNotesFollowingFolder = false; + mIsOneNoteFollowingFolder = false; + + if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + int position = cursor.getPosition(); + if (cursor.moveToPrevious()) { + if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER + || cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) { + if (cursor.getCount() > (position + 1)) { + mIsMultiNotesFollowingFolder = true; + } else { + mIsOneNoteFollowingFolder = true; + } + } + if (!cursor.moveToNext()) { + throw new IllegalStateException("cursor move to previous but can't move back"); + } + } + } + } + + public boolean isOneFollowingFolder() { + return mIsOneNoteFollowingFolder; + } + + public boolean isMultiFollowingFolder() { + return mIsMultiNotesFollowingFolder; + } + + public boolean isLast() { + return mIsLastItem; + } + + public String getCallName() { + return mName; + } + + public boolean isFirst() { + return mIsFirstItem; + } + + public boolean isSingle() { + return mIsOnlyOneItem; + } + + public long getId() { + return mId; + } + + public long getAlertDate() { + return mAlertDate; + } + + public long getCreatedDate() { + return mCreatedDate; + } + + public boolean hasAttachment() { + return mHasAttachment; + } + + public long getModifiedDate() { + return mModifiedDate; + } + + public int getBgColorId() { + return mBgColorId; + } + + public long getParentId() { + return mParentId; + } + + public int getNotesCount() { + return mNotesCount; + } + + public long getFolderId () { + return mParentId; + } + + public int getType() { + return mType; + } + + public int getWidgetType() { + return mWidgetType; + } + + public int getWidgetId() { + return mWidgetId; + } + + public String getSnippet() { + return mSnippet; + } + + public boolean hasAlert() { + return (mAlertDate > 0); + } + + public boolean isCallRecord() { + return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber)); + } + + public static int getNoteType(Cursor cursor) { + return cursor.getInt(TYPE_COLUMN); + } +} diff --git a/src/net/micode/notes/ui/NotesListActivity.java b/src/net/micode/notes/ui/NotesListActivity.java new file mode 100644 index 0000000..e843aec --- /dev/null +++ b/src/net/micode/notes/ui/NotesListActivity.java @@ -0,0 +1,954 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.appwidget.AppWidgetManager; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Display; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MenuItem.OnMenuItemClickListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; +import android.view.View.OnTouchListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; +import net.micode.notes.model.WorkingNote; +import net.micode.notes.tool.BackupUtils; +import net.micode.notes.tool.DataUtils; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; +import net.micode.notes.widget.NoteWidgetProvider_2x; +import net.micode.notes.widget.NoteWidgetProvider_4x; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashSet; + +public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener { + private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; + + private static final int FOLDER_LIST_QUERY_TOKEN = 1; + + private static final int MENU_FOLDER_DELETE = 0; + + private static final int MENU_FOLDER_VIEW = 1; + + private static final int MENU_FOLDER_CHANGE_NAME = 2; + + private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; + + private enum ListEditState { + NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER + }; + + private ListEditState mState; + + private BackgroundQueryHandler mBackgroundQueryHandler; + + private NotesListAdapter mNotesListAdapter; + + private ListView mNotesListView; + + private Button mAddNewNote; + + private boolean mDispatch; + + private int mOriginY; + + private int mDispatchY; + + private TextView mTitleBar; + + private long mCurrentFolderId; + + private ContentResolver mContentResolver; + + private ModeCallback mModeCallBack; + + private static final String TAG = "NotesListActivity"; + + public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; + + private NoteItemData mFocusNoteDataItem; + + private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; + + private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>" + + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + + NoteColumns.NOTES_COUNT + ">0)"; + + private final static int REQUEST_CODE_OPEN_NODE = 102; + private final static int REQUEST_CODE_NEW_NODE = 103; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.note_list); + initResources(); + + /** + * Insert an introduction when user firstly use this application + */ + setAppInfoFromRawRes(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK + && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) { + mNotesListAdapter.changeCursor(null); + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void setAppInfoFromRawRes() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) { + StringBuilder sb = new StringBuilder(); + InputStream in = null; + try { + in = getResources().openRawResource(R.raw.introduction); + if (in != null) { + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + char [] buf = new char[1024]; + int len = 0; + while ((len = br.read(buf)) > 0) { + sb.append(buf, 0, len); + } + } else { + Log.e(TAG, "Read introduction file error"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + return; + } finally { + if(in != null) { + try { + in.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER, + AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE, + ResourceParser.RED); + note.setWorkingText(sb.toString()); + if (note.saveNote()) { + sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit(); + } else { + Log.e(TAG, "Save introduction note error"); + return; + } + } + } + + @Override + protected void onStart() { + super.onStart(); + startAsyncNotesListQuery(); + } + + private void initResources() { + mContentResolver = this.getContentResolver(); + mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver()); + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mNotesListView = (ListView) findViewById(R.id.notes_list); + mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null), + null, false); + mNotesListView.setOnItemClickListener(new OnListItemClickListener()); + mNotesListView.setOnItemLongClickListener(this); + mNotesListAdapter = new NotesListAdapter(this); + mNotesListView.setAdapter(mNotesListAdapter); + mAddNewNote = (Button) findViewById(R.id.btn_new_note); + mAddNewNote.setOnClickListener(this); + mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); + mDispatch = false; + mDispatchY = 0; + mOriginY = 0; + mTitleBar = (TextView) findViewById(R.id.tv_title_bar); + mState = ListEditState.NOTE_LIST; + mModeCallBack = new ModeCallback(); + } + + private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener { + private DropdownMenu mDropDownMenu; + private ActionMode mActionMode; + private MenuItem mMoveMenu; + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + getMenuInflater().inflate(R.menu.note_list_options, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(this); + mMoveMenu = menu.findItem(R.id.move); + if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER + || DataUtils.getUserFolderCount(mContentResolver) == 0) { + mMoveMenu.setVisible(false); + } else { + mMoveMenu.setVisible(true); + mMoveMenu.setOnMenuItemClickListener(this); + } + mActionMode = mode; + mNotesListAdapter.setChoiceMode(true); + mNotesListView.setLongClickable(false); + mAddNewNote.setVisibility(View.GONE); + + View customView = LayoutInflater.from(NotesListActivity.this).inflate( + R.layout.note_list_dropdown_menu, null); + mode.setCustomView(customView); + mDropDownMenu = new DropdownMenu(NotesListActivity.this, + (Button) customView.findViewById(R.id.selection_menu), + R.menu.note_list_dropdown); + mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ + public boolean onMenuItemClick(MenuItem item) { + mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected()); + updateMenu(); + return true; + } + + }); + return true; + } + + private void updateMenu() { + int selectedCount = mNotesListAdapter.getSelectedCount(); + // Update dropdown menu + String format = getResources().getString(R.string.menu_select_title, selectedCount); + mDropDownMenu.setTitle(format); + MenuItem item = mDropDownMenu.findItem(R.id.action_select_all); + if (item != null) { + if (mNotesListAdapter.isAllSelected()) { + item.setChecked(true); + item.setTitle(R.string.menu_deselect_all); + } else { + item.setChecked(false); + item.setTitle(R.string.menu_select_all); + } + } + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + // TODO Auto-generated method stub + return false; + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + // TODO Auto-generated method stub + return false; + } + + public void onDestroyActionMode(ActionMode mode) { + mNotesListAdapter.setChoiceMode(false); + mNotesListView.setLongClickable(true); + mAddNewNote.setVisibility(View.VISIBLE); + } + + public void finishActionMode() { + mActionMode.finish(); + } + + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, + boolean checked) { + mNotesListAdapter.setCheckedItem(position, checked); + updateMenu(); + } + + public boolean onMenuItemClick(MenuItem item) { + if (mNotesListAdapter.getSelectedCount() == 0) { + Toast.makeText(NotesListActivity.this, getString(R.string.menu_select_none), + Toast.LENGTH_SHORT).show(); + return true; + } + + switch (item.getItemId()) { + case R.id.delete: + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_notes, + mNotesListAdapter.getSelectedCount())); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, + int which) { + batchDelete(); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case R.id.move: + startQueryDestinationFolders(); + break; + default: + return false; + } + return true; + } + } + + private class NewNoteOnTouchListener implements OnTouchListener { + + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + Display display = getWindowManager().getDefaultDisplay(); + int screenHeight = display.getHeight(); + int newNoteViewHeight = mAddNewNote.getHeight(); + int start = screenHeight - newNoteViewHeight; + int eventY = start + (int) event.getY(); + /** + * Minus TitleBar's height + */ + if (mState == ListEditState.SUB_FOLDER) { + eventY -= mTitleBar.getHeight(); + start -= mTitleBar.getHeight(); + } + /** + * HACKME:When click the transparent part of "New Note" button, dispatch + * the event to the list view behind this button. The transparent part of + * "New Note" button could be expressed by formula y=-0.12x+94(Unit:pixel) + * and the line top of the button. The coordinate based on left of the "New + * Note" button. The 94 represents maximum height of the transparent part. + * Notice that, if the background of the button changes, the formula should + * also change. This is very bad, just for the UI designer's strong requirement. + */ + if (event.getY() < (event.getX() * (-0.12) + 94)) { + View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1 + - mNotesListView.getFooterViewsCount()); + if (view != null && view.getBottom() > start + && (view.getTop() < (start + 94))) { + mOriginY = (int) event.getY(); + mDispatchY = eventY; + event.setLocation(event.getX(), mDispatchY); + mDispatch = true; + return mNotesListView.dispatchTouchEvent(event); + } + } + break; + } + case MotionEvent.ACTION_MOVE: { + if (mDispatch) { + mDispatchY += (int) event.getY() - mOriginY; + event.setLocation(event.getX(), mDispatchY); + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + default: { + if (mDispatch) { + event.setLocation(event.getX(), mDispatchY); + mDispatch = false; + return mNotesListView.dispatchTouchEvent(event); + } + break; + } + } + return false; + } + + }; + + private void startAsyncNotesListQuery() { + String selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION + : NORMAL_SELECTION; + mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null, + Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] { + String.valueOf(mCurrentFolderId) + }, NoteColumns.TYPE + " DESC," + NoteColumns.MODIFIED_DATE + " DESC"); + } + + private final class BackgroundQueryHandler extends AsyncQueryHandler { + public BackgroundQueryHandler(ContentResolver contentResolver) { + super(contentResolver); + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + switch (token) { + case FOLDER_NOTE_LIST_QUERY_TOKEN: + mNotesListAdapter.changeCursor(cursor); + break; + case FOLDER_LIST_QUERY_TOKEN: + if (cursor != null && cursor.getCount() > 0) { + showFolderListMenu(cursor); + } else { + Log.e(TAG, "Query folder failed"); + } + break; + default: + return; + } + } + } + + private void showFolderListMenu(Cursor cursor) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(R.string.menu_title_select_folder); + final FoldersListAdapter adapter = new FoldersListAdapter(this, cursor); + builder.setAdapter(adapter, new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + DataUtils.batchMoveToFolder(mContentResolver, + mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which)); + Toast.makeText( + NotesListActivity.this, + getString(R.string.format_move_notes_to_folder, + mNotesListAdapter.getSelectedCount(), + adapter.getFolderName(NotesListActivity.this, which)), + Toast.LENGTH_SHORT).show(); + mModeCallBack.finishActionMode(); + } + }); + builder.show(); + } + + private void createNewNote() { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId); + this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + } + + private void batchDelete() { + new AsyncTask>() { + protected HashSet doInBackground(Void... unused) { + HashSet widgets = mNotesListAdapter.getSelectedWidget(); + if (!isSyncMode()) { + // if not synced, delete notes directly + if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter + .getSelectedItemIds())) { + } else { + Log.e(TAG, "Delete notes error, should not happens"); + } + } else { + // in sync mode, we'll move the deleted note into the trash + // folder + if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter + .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) { + Log.e(TAG, "Move notes to trash folder error, should not happens"); + } + } + return widgets; + } + + @Override + protected void onPostExecute(HashSet widgets) { + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + mModeCallBack.finishActionMode(); + } + }.execute(); + } + + private void deleteFolder(long folderId) { + if (folderId == Notes.ID_ROOT_FOLDER) { + Log.e(TAG, "Wrong folder id, should not happen " + folderId); + return; + } + + HashSet ids = new HashSet(); + ids.add(folderId); + HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver, + folderId); + if (!isSyncMode()) { + // if not synced, delete folder directly + DataUtils.batchDeleteNotes(mContentResolver, ids); + } else { + // in sync mode, we'll move the deleted folder into the trash folder + DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER); + } + if (widgets != null) { + for (AppWidgetAttribute widget : widgets) { + if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID + && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) { + updateWidget(widget.widgetId, widget.widgetType); + } + } + } + } + + private void openNode(NoteItemData data) { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, data.getId()); + this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + private void openFolder(NoteItemData data) { + mCurrentFolderId = data.getId(); + startAsyncNotesListQuery(); + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mState = ListEditState.CALL_RECORD_FOLDER; + mAddNewNote.setVisibility(View.GONE); + } else { + mState = ListEditState.SUB_FOLDER; + } + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mTitleBar.setText(R.string.call_record_folder_name); + } else { + mTitleBar.setText(data.getSnippet()); + } + mTitleBar.setVisibility(View.VISIBLE); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.btn_new_note: + createNewNote(); + break; + default: + break; + } + } + + private void showSoftInput() { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + } + } + + private void hideSoftInput(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showCreateOrModifyFolderDialog(final boolean create) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null); + final EditText etName = (EditText) view.findViewById(R.id.et_foler_name); + showSoftInput(); + if (!create) { + if (mFocusNoteDataItem != null) { + etName.setText(mFocusNoteDataItem.getSnippet()); + builder.setTitle(getString(R.string.menu_folder_change_name)); + } else { + Log.e(TAG, "The long click data item is null"); + return; + } + } else { + etName.setText(""); + builder.setTitle(this.getString(R.string.menu_create_folder)); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + hideSoftInput(etName); + } + }); + + final Dialog dialog = builder.setView(view).show(); + final Button positive = (Button)dialog.findViewById(android.R.id.button1); + positive.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + hideSoftInput(etName); + String name = etName.getText().toString(); + if (DataUtils.checkVisibleFolderName(mContentResolver, name)) { + Toast.makeText(NotesListActivity.this, getString(R.string.folder_exist, name), + Toast.LENGTH_LONG).show(); + etName.setSelection(0, etName.length()); + return; + } + if (!create) { + if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + + "=?", new String[] { + String.valueOf(mFocusNoteDataItem.getId()) + }); + } + } else if (!TextUtils.isEmpty(name)) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + mContentResolver.insert(Notes.CONTENT_NOTE_URI, values); + } + dialog.dismiss(); + } + }); + + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } + /** + * When the name edit text is null, disable the positive button + */ + etName.addTextChangedListener(new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // TODO Auto-generated method stub + + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (TextUtils.isEmpty(etName.getText())) { + positive.setEnabled(false); + } else { + positive.setEnabled(true); + } + } + + public void afterTextChanged(Editable s) { + // TODO Auto-generated method stub + + } + }); + } + + @Override + public void onBackPressed() { + switch (mState) { + case SUB_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + startAsyncNotesListQuery(); + mTitleBar.setVisibility(View.GONE); + break; + case CALL_RECORD_FOLDER: + mCurrentFolderId = Notes.ID_ROOT_FOLDER; + mState = ListEditState.NOTE_LIST; + mAddNewNote.setVisibility(View.VISIBLE); + mTitleBar.setVisibility(View.GONE); + startAsyncNotesListQuery(); + break; + case NOTE_LIST: + super.onBackPressed(); + break; + default: + break; + } + } + + private void updateWidget(int appWidgetId, int appWidgetType) { + Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + if (appWidgetType == Notes.TYPE_WIDGET_2X) { + intent.setClass(this, NoteWidgetProvider_2x.class); + } else if (appWidgetType == Notes.TYPE_WIDGET_4X) { + intent.setClass(this, NoteWidgetProvider_4x.class); + } else { + Log.e(TAG, "Unspported widget type"); + return; + } + + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] { + appWidgetId + }); + + sendBroadcast(intent); + setResult(RESULT_OK, intent); + } + + private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + if (mFocusNoteDataItem != null) { + menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); + menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view); + menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete); + menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name); + } + } + }; + + @Override + public void onContextMenuClosed(Menu menu) { + if (mNotesListView != null) { + mNotesListView.setOnCreateContextMenuListener(null); + } + super.onContextMenuClosed(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mFocusNoteDataItem == null) { + Log.e(TAG, "The long click data item is null"); + return false; + } + switch (item.getItemId()) { + case MENU_FOLDER_VIEW: + openFolder(mFocusNoteDataItem); + break; + case MENU_FOLDER_DELETE: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.alert_title_delete)); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setMessage(getString(R.string.alert_message_delete_folder)); + builder.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + deleteFolder(mFocusNoteDataItem.getId()); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + break; + case MENU_FOLDER_CHANGE_NAME: + showCreateOrModifyFolderDialog(false); + break; + default: + break; + } + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.clear(); + if (mState == ListEditState.NOTE_LIST) { + getMenuInflater().inflate(R.menu.note_list, menu); + // set sync or sync_cancel + menu.findItem(R.id.menu_sync).setTitle( + GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync); + } else if (mState == ListEditState.SUB_FOLDER) { + getMenuInflater().inflate(R.menu.sub_folder, menu); + } else if (mState == ListEditState.CALL_RECORD_FOLDER) { + getMenuInflater().inflate(R.menu.call_record_folder, menu); + } else { + Log.e(TAG, "Wrong state:" + mState); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_new_folder: { + showCreateOrModifyFolderDialog(true); + break; + } + case R.id.menu_export_text: { + exportNoteToText(); + break; + } + case R.id.menu_sync: { + if (isSyncMode()) { + if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) { + GTaskSyncService.startSync(this); + } else { + GTaskSyncService.cancelSync(this); + } + } else { + startPreferenceActivity(); + } + break; + } + case R.id.menu_setting: { + startPreferenceActivity(); + break; + } + case R.id.menu_new_note: { + createNewNote(); + break; + } + case R.id.menu_search: + onSearchRequested(); + break; + default: + break; + } + return true; + } + + @Override + public boolean onSearchRequested() { + startSearch(null, false, null /* appData */, false); + return true; + } + + private void exportNoteToText() { + final BackupUtils backup = BackupUtils.getInstance(NotesListActivity.this); + new AsyncTask() { + + @Override + protected Integer doInBackground(Void... unused) { + return backup.exportToText(); + } + + @Override + protected void onPostExecute(Integer result) { + if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_unmounted)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SUCCESS) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.success_sdcard_export)); + builder.setMessage(NotesListActivity.this.getString( + R.string.format_exported_file_location, backup + .getExportedTextFileName(), backup.getExportedTextFileDir())); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } else if (result == BackupUtils.STATE_SYSTEM_ERROR) { + AlertDialog.Builder builder = new AlertDialog.Builder(NotesListActivity.this); + builder.setTitle(NotesListActivity.this + .getString(R.string.failed_sdcard_export)); + builder.setMessage(NotesListActivity.this + .getString(R.string.error_sdcard_export)); + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + } + + }.execute(); + } + + private boolean isSyncMode() { + return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0; + } + + private void startPreferenceActivity() { + Activity from = getParent() != null ? getParent() : this; + Intent intent = new Intent(from, NotesPreferenceActivity.class); + from.startActivityIfNeeded(intent, -1); + } + + private class OnListItemClickListener implements OnItemClickListener { + + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + NoteItemData item = ((NotesListItem) view).getItemData(); + if (mNotesListAdapter.isInChoiceMode()) { + if (item.getType() == Notes.TYPE_NOTE) { + position = position - mNotesListView.getHeaderViewsCount(); + mModeCallBack.onItemCheckedStateChanged(null, position, id, + !mNotesListAdapter.isSelectedItem(position)); + } + return; + } + + switch (mState) { + case NOTE_LIST: + if (item.getType() == Notes.TYPE_FOLDER + || item.getType() == Notes.TYPE_SYSTEM) { + openFolder(item); + } else if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in NOTE_LIST"); + } + break; + case SUB_FOLDER: + case CALL_RECORD_FOLDER: + if (item.getType() == Notes.TYPE_NOTE) { + openNode(item); + } else { + Log.e(TAG, "Wrong note type in SUB_FOLDER"); + } + break; + default: + break; + } + } + } + + } + + private void startQueryDestinationFolders() { + String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?"; + selection = (mState == ListEditState.NOTE_LIST) ? selection: + "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")"; + + mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN, + null, + Notes.CONTENT_NOTE_URI, + FoldersListAdapter.PROJECTION, + selection, + new String[] { + String.valueOf(Notes.TYPE_FOLDER), + String.valueOf(Notes.ID_TRASH_FOLER), + String.valueOf(mCurrentFolderId) + }, + NoteColumns.MODIFIED_DATE + " DESC"); + } + + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (view instanceof NotesListItem) { + mFocusNoteDataItem = ((NotesListItem) view).getItemData(); + if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) { + if (mNotesListView.startActionMode(mModeCallBack) != null) { + mModeCallBack.onItemCheckedStateChanged(null, position, id, true); + mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } else { + Log.e(TAG, "startActionMode fails"); + } + } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) { + mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener); + } + } + return false; + } +} diff --git a/src/net/micode/notes/ui/NotesListAdapter.java b/src/net/micode/notes/ui/NotesListAdapter.java new file mode 100644 index 0000000..51c9cb9 --- /dev/null +++ b/src/net/micode/notes/ui/NotesListAdapter.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; + +import net.micode.notes.data.Notes; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + + +public class NotesListAdapter extends CursorAdapter { + private static final String TAG = "NotesListAdapter"; + private Context mContext; + private HashMap mSelectedIndex; + private int mNotesCount; + private boolean mChoiceMode; + + public static class AppWidgetAttribute { + public int widgetId; + public int widgetType; + }; + + public NotesListAdapter(Context context) { + super(context, null); + mSelectedIndex = new HashMap(); + mContext = context; + mNotesCount = 0; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return new NotesListItem(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + if (view instanceof NotesListItem) { + NoteItemData itemData = new NoteItemData(context, cursor); + ((NotesListItem) view).bind(context, itemData, mChoiceMode, + isSelectedItem(cursor.getPosition())); + } + } + + public void setCheckedItem(final int position, final boolean checked) { + mSelectedIndex.put(position, checked); + notifyDataSetChanged(); + } + + public boolean isInChoiceMode() { + return mChoiceMode; + } + + public void setChoiceMode(boolean mode) { + mSelectedIndex.clear(); + mChoiceMode = mode; + } + + public void selectAll(boolean checked) { + Cursor cursor = getCursor(); + for (int i = 0; i < getCount(); i++) { + if (cursor.moveToPosition(i)) { + if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { + setCheckedItem(i, checked); + } + } + } + } + + public HashSet getSelectedItemIds() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Long id = getItemId(position); + if (id == Notes.ID_ROOT_FOLDER) { + Log.d(TAG, "Wrong item id, should not happen"); + } else { + itemSet.add(id); + } + } + } + + return itemSet; + } + + public HashSet getSelectedWidget() { + HashSet itemSet = new HashSet(); + for (Integer position : mSelectedIndex.keySet()) { + if (mSelectedIndex.get(position) == true) { + Cursor c = (Cursor) getItem(position); + if (c != null) { + AppWidgetAttribute widget = new AppWidgetAttribute(); + NoteItemData item = new NoteItemData(mContext, c); + widget.widgetId = item.getWidgetId(); + widget.widgetType = item.getWidgetType(); + itemSet.add(widget); + /** + * Don't close cursor here, only the adapter could close it + */ + } else { + Log.e(TAG, "Invalid cursor"); + return null; + } + } + } + return itemSet; + } + + public int getSelectedCount() { + Collection values = mSelectedIndex.values(); + if (null == values) { + return 0; + } + Iterator iter = values.iterator(); + int count = 0; + while (iter.hasNext()) { + if (true == iter.next()) { + count++; + } + } + return count; + } + + public boolean isAllSelected() { + int checkedCount = getSelectedCount(); + return (checkedCount != 0 && checkedCount == mNotesCount); + } + + public boolean isSelectedItem(final int position) { + if (null == mSelectedIndex.get(position)) { + return false; + } + return mSelectedIndex.get(position); + } + + @Override + protected void onContentChanged() { + super.onContentChanged(); + calcNotesCount(); + } + + @Override + public void changeCursor(Cursor cursor) { + super.changeCursor(cursor); + calcNotesCount(); + } + + private void calcNotesCount() { + mNotesCount = 0; + for (int i = 0; i < getCount(); i++) { + Cursor c = (Cursor) getItem(i); + if (c != null) { + if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { + mNotesCount++; + } + } else { + Log.e(TAG, "Invalid cursor"); + return; + } + } + } +} diff --git a/src/net/micode/notes/ui/NotesListItem.java b/src/net/micode/notes/ui/NotesListItem.java new file mode 100644 index 0000000..1221e80 --- /dev/null +++ b/src/net/micode/notes/ui/NotesListItem.java @@ -0,0 +1,122 @@ +/* + * 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); + inflate(context, R.layout.note_item, this); + mAlert = (ImageView) findViewById(R.id.iv_alert_icon); + mTitle = (TextView) findViewById(R.id.tv_title); + mTime = (TextView) findViewById(R.id.tv_time); + mCallName = (TextView) findViewById(R.id.tv_name); + mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); + } + + public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + mCheckBox.setVisibility(View.VISIBLE); + mCheckBox.setChecked(checked); + } else { + mCheckBox.setVisibility(View.GONE); + } + + mItemData = data; + if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.GONE); + mAlert.setVisibility(View.VISIBLE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + mTitle.setText(context.getString(R.string.call_record_folder_name) + + context.getString(R.string.format_folder_files_count, data.getNotesCount())); + mAlert.setImageResource(R.drawable.call_record); + } else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { + mCallName.setVisibility(View.VISIBLE); + mCallName.setText(data.getCallName()); + mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem); + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } else { + mCallName.setVisibility(View.GONE); + mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); + + if (data.getType() == Notes.TYPE_FOLDER) { + mTitle.setText(data.getSnippet() + + context.getString(R.string.format_folder_files_count, + data.getNotesCount())); + mAlert.setVisibility(View.GONE); + } else { + mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); + if (data.hasAlert()) { + mAlert.setImageResource(R.drawable.clock); + mAlert.setVisibility(View.VISIBLE); + } else { + mAlert.setVisibility(View.GONE); + } + } + } + mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + setBackground(data); + } + + private void setBackground(NoteItemData data) { + int id = data.getBgColorId(); + if (data.getType() == Notes.TYPE_NOTE) { + if (data.isSingle() || data.isOneFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); + } else if (data.isLast()) { + setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); + } else if (data.isFirst() || data.isMultiFollowingFolder()) { + setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); + } else { + setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); + } + } else { + setBackgroundResource(NoteItemBgResources.getFolderBgRes()); + } + } + + public NoteItemData getItemData() { + return mItemData; + } +} diff --git a/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/net/micode/notes/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..07c5f7e --- /dev/null +++ b/src/net/micode/notes/ui/NotesPreferenceActivity.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.ui; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + + +public class NotesPreferenceActivity extends PreferenceActivity { + public static final String PREFERENCE_NAME = "notes_preferences"; + + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; + + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; + + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; + + private static final String AUTHORITIES_FILTER_KEY = "authorities"; + + private PreferenceCategory mAccountCategory; + + private GTaskReceiver mReceiver; + + private Account[] mOriAccounts; + + private boolean mHasAddedAccount; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + addPreferencesFromResource(R.xml.preferences); + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter); + + mOriAccounts = null; + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true); + } + + @Override + protected void onResume() { + super.onResume(); + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); + } + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + } + + private void loadAccountPreference() { + mAccountCategory.removeAll(); + + Preference accountPref = new Preference(this); + final String defaultAccount = getSyncAccountName(this); + accountPref.setTitle(getString(R.string.preferences_account_title)); + accountPref.setSummary(getString(R.string.preferences_account_summary)); + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog(); + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + } + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show(); + } + return true; + } + }); + + mAccountCategory.addPreference(accountPref); + } + + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button); + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately)); + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + } + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + } + + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + } + + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); + + dialogBuilder.setCustomTitle(titleView); + dialogBuilder.setPositiveButton(null, null); + + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this); + + mOriAccounts = accounts; + mHasAddedAccount = false; + + if (accounts.length > 0) { + CharSequence[] items = new CharSequence[accounts.length]; + final CharSequence[] itemMapping = items; + int checkedItem = -1; + int index = 0; + for (Account account : accounts) { + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + } + items[index++] = account.name; + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); + dialog.dismiss(); + refreshUI(); + } + }); + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); + dialogBuilder.setView(addAccountView); + + final AlertDialog dialog = dialogBuilder.show(); + addAccountView.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + mHasAddedAccount = true; + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + }); + startActivityForResult(intent, -1); + dialog.dismiss(); + } + }); + } + + private void showChangeAccountConfirmAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + dialogBuilder.setCustomTitle(titleView); + + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + showSelectAccountAlertDialog(); + } else if (which == 1) { + removeSyncAccount(); + refreshUI(); + } + } + }); + dialogBuilder.show(); + } + + private Account[] getGoogleAccounts() { + AccountManager accountManager = AccountManager.get(this); + return accountManager.getAccountsByType("com.google"); + } + + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + editor.commit(); + + // clean up last sync time + setLastSyncTime(this, 0); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show(); + } + } + + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + } + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); + } + editor.commit(); + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start(); + } + + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); + } + + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); + editor.commit(); + } + + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); + } + + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI(); + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); + } + + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + default: + return false; + } + } +} diff --git a/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/net/micode/notes/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..ec6f819 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/net/micode/notes/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..adcb2f7 --- /dev/null +++ b/src/net/micode/notes/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/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/net/micode/notes/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..c12a02e --- /dev/null +++ b/src/net/micode/notes/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; + } +} From 1d2f5c1d550dcf1fba214a8556f90591433a325d Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Wed, 4 Dec 2024 23:25:49 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/net/micode/notes/gtask/data/MetaData.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index 3a2050b..b0bbe62 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -18,7 +18,7 @@ package net.micode.notes.gtask.data; import android.database.Cursor; import android.util.Log; - +import javafx.concurrent.Task; import net.micode.notes.tool.GTaskStringUtils; import org.json.JSONException; @@ -27,7 +27,7 @@ import org.json.JSONObject; public class MetaData extends Task { private final static String TAG = MetaData.class.getSimpleName(); - +//调用getSimpleName ()函数,将得到类的简写名称存入字符串TAG中 private String mRelatedGid = null; public void setMeta(String gid, JSONObject metaInfo) { From c17b8df8b72339f706ac6dd9431e93d3d7e1e3f4 Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Fri, 6 Dec 2024 21:28:35 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=B3=A8=E9=87=8Aing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/net/micode/notes/gtask/data/MetaData.java | 15 ++++++++++++--- src/net/micode/notes/gtask/data/Node.java | 1 + src/net/micode/notes/gtask/data/SqlData.java | 9 ++++++++- src/net/micode/notes/gtask/data/SqlNote.java | 4 ++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/net/micode/notes/gtask/data/MetaData.java b/src/net/micode/notes/gtask/data/MetaData.java index b0bbe62..0789be1 100644 --- a/src/net/micode/notes/gtask/data/MetaData.java +++ b/src/net/micode/notes/gtask/data/MetaData.java @@ -31,33 +31,39 @@ public class MetaData extends Task { private String mRelatedGid = null; public void setMeta(String gid, JSONObject metaInfo) { +//调用JSONObject库函数put (),Task类中的setNotes ()和setName ()函数生成元数据库 + try { metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid); } catch (JSONException e) { - Log.e(TAG, "failed to put related gid"); + Log.e(TAG, "failed to put related gid");//输出错误信息 } setNotes(metaInfo.toString()); setName(GTaskStringUtils.META_NOTE_NAME); } public String getRelatedGid() { +//获取相关联的Gid return mRelatedGid; } @Override public boolean isWorthSaving() { +//判断当前数据是否为空,若为空则返回真即值得保存 return getNotes() != null; } @Override public void setContentByRemoteJSON(JSONObject js) { +//使用远程json数据对象设置元数据内容 super.setContentByRemoteJSON(js); if (getNotes() != null) { try { JSONObject metaInfo = new JSONObject(getNotes().trim()); mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID); } catch (JSONException e) { - Log.w(TAG, "failed to get related gid"); + Log.w(TAG, "failed to get related gid");//输出警告信息 + mRelatedGid = null; } } @@ -66,16 +72,19 @@ public class MetaData extends Task { @Override public void setContentByLocalJSON(JSONObject js) { // this function should not be called - throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called"); +//使用本地json数据对象设置元数据内容 + throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");//传递非法参数异常 } @Override public JSONObject getLocalJSONFromContent() { +//从元数据内容中获取本地json对象 throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called"); } @Override public int getSyncAction(Cursor c) { +//获取同步动作状态 throw new IllegalAccessError("MetaData:getSyncAction should not be called"); } diff --git a/src/net/micode/notes/gtask/data/Node.java b/src/net/micode/notes/gtask/data/Node.java index 63950e0..48dee83 100644 --- a/src/net/micode/notes/gtask/data/Node.java +++ b/src/net/micode/notes/gtask/data/Node.java @@ -21,6 +21,7 @@ import android.database.Cursor; import org.json.JSONObject; public abstract class Node { +//定义了各种用于表征同步状态的常量 public static final int SYNC_ACTION_NONE = 0; public static final int SYNC_ACTION_ADD_REMOTE = 1; diff --git a/src/net/micode/notes/gtask/data/SqlData.java b/src/net/micode/notes/gtask/data/SqlData.java index d3ec3be..b502e04 100644 --- a/src/net/micode/notes/gtask/data/SqlData.java +++ b/src/net/micode/notes/gtask/data/SqlData.java @@ -36,6 +36,7 @@ import org.json.JSONObject; public class SqlData { +//得到类的简写名称存入字符串TAG中 private static final String TAG = SqlData.class.getSimpleName(); private static final int INVALID_ID = -99999; @@ -72,6 +73,7 @@ public class SqlData { private ContentValues mDiffDataValues; public SqlData(Context context) { +//构造函数,用于初始化数据 mContentResolver = context.getContentResolver(); mIsCreate = true; mDataId = INVALID_ID; @@ -83,6 +85,7 @@ public class SqlData { } public SqlData(Context context, Cursor c) { +//构造函数,用于初始化数据 mContentResolver = context.getContentResolver(); mIsCreate = false; loadFromCursor(c); @@ -90,6 +93,7 @@ public class SqlData { } private void loadFromCursor(Cursor c) { +//从当前的光标处将五列的数据加载到该类的对象 mDataId = c.getLong(DATA_ID_COLUMN); mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN); mDataContent = c.getString(DATA_CONTENT_COLUMN); @@ -98,6 +102,7 @@ public class SqlData { } public void setContent(JSONObject js) throws JSONException { +//设置用于共享的数据 long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID; if (mIsCreate || mDataId != dataId) { mDiffDataValues.put(DataColumns.ID, dataId); @@ -131,6 +136,7 @@ public class SqlData { } public JSONObject getContent() throws JSONException { +//获取共享的数据内容 if (mIsCreate) { Log.e(TAG, "it seems that we haven't created this in database yet"); return null; @@ -145,7 +151,7 @@ public class SqlData { } public void commit(long noteId, boolean validateVersion, long version) { - +//把当前造作所做的修改保存到数据库 if (mIsCreate) { if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) { mDiffDataValues.remove(DataColumns.ID); @@ -184,6 +190,7 @@ public class SqlData { } public long getId() { +//获取当前id return mDataId; } } diff --git a/src/net/micode/notes/gtask/data/SqlNote.java b/src/net/micode/notes/gtask/data/SqlNote.java index 79a4095..c10e219 100644 --- a/src/net/micode/notes/gtask/data/SqlNote.java +++ b/src/net/micode/notes/gtask/data/SqlNote.java @@ -39,6 +39,7 @@ import java.util.ArrayList; public class SqlNote { +//得到类的简写名称存入字符串TAG中 private static final String TAG = SqlNote.class.getSimpleName(); private static final int INVALID_ID = -99999; @@ -123,6 +124,7 @@ public class SqlNote { private ArrayList mDataList; public SqlNote(Context context) { +//构造函数 mContext = context; mContentResolver = context.getContentResolver(); mIsCreate = true; @@ -144,6 +146,7 @@ public class SqlNote { } public SqlNote(Context context, Cursor c) { +//构造函数 mContext = context; mContentResolver = context.getContentResolver(); mIsCreate = false; @@ -167,6 +170,7 @@ public class SqlNote { } private void loadFromCursor(long id) { +// Cursor c = null; try { c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)", From 72cd45f1a2f883b319b4a657ce5c0b2a68c2165e Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Sat, 21 Dec 2024 13:54:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E6=B3=9B=E8=AF=BB=E6=8A=A5=E5=91=8A3?= =?UTF-8?q?=E7=BC=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...米便签开源代码的泛读报告.docx | Bin 16698 -> 229030 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码的泛读报告.docx b/doc/小米便签开源代码的泛读报告.docx index 92cdf698ed0aae8d6f97df93d97d27e6fe08e660..14a9a9d0fd33256d7dc9a07aac7f0d3103cdfaf3 100644 GIT binary patch literal 229030 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

q{Gr@}rt z3X3ct2t9E-cLsao?n!DvTCXvZ5#uj1νuagO=lEhWKPc?59quGKsAb&9q<-5iDL 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
qYYDH%?w!K;r1a)b4YhyD0VgSdJH}(`yflMZQ*e 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

  • 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 F{{gUsbC>`C From eca97045c7ceba581a7c589dd7719aaaedbda20f Mon Sep 17 00:00:00 2001 From: 1 <1> Date: Fri, 27 Dec 2024 00:00:12 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E5=88=86=E6=9E=90=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...签开源代码的质量分析报告.docx | Bin 13911 -> 25107 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/小米便签开源代码的质量分析报告.docx b/doc/小米便签开源代码的质量分析报告.docx index 45f911a6901431655901987f1bdf9f2d53d75f8f..43b5ce26039406156874d74d4777c075eb5825c7 100644 GIT binary patch literal 25107 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