新增搜索便签功能 #25

Closed
p7tupf26b wants to merge 2 commits from jiangtianxiang_branch into master

@ -13,3 +13,8 @@
.externalNativeBuild
.cxx
local.properties
build.gradle.kts
gradle.properties
gradlew
gradlew.bat
settings.gradle.kts

@ -91,6 +91,15 @@
android:resource="@xml/searchable" />
</activity>
<!-- ==================== 搜索活动 ==================== -->
<activity
android:name=".ui.NoteSearchActivity"
android:label="@string/search"
android:launchMode="singleTop"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false" />
<!-- ==================== 内容提供者 ==================== -->
<!-- 提供笔记数据的访问接口,允许其他应用访问笔记数据 -->
<provider
@ -182,12 +191,16 @@
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>
<!-- ==================== 同步服务 ==================== -->
<!-- Google任务同步服务用于与Google Tasks同步数据 -->
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<!-- ==================== 同步服务 ==================== -->
<!-- Google任务同步服务用于与Google Tasks同步数据 -->
<!-- 暂时禁用同步功能,为未来云同步开发暂留代码 -->
<!-- 若需启用请在build.gradle.kts中移除sourceSets.exclude("**/gtask/**")并添加Apache HttpClient依赖 -->
<!--
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
-->
<!-- ==================== 搜索元数据 ==================== -->
<!-- 指定默认的搜索活动为NoteEditActivity -->

@ -14,6 +14,7 @@ import androidx.core.view.WindowInsetsCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import net.micode.notes.data.Notes;
import net.micode.notes.databinding.ActivityMainBinding;
import net.micode.notes.ui.SidebarFragment;
/**
@ -26,7 +27,7 @@ import net.micode.notes.ui.SidebarFragment;
public class MainActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener {
private static final String TAG = "MainActivity";
private DrawerLayout drawerLayout;
private ActivityMainBinding binding;
/**
*
@ -41,16 +42,16 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O
super.onCreate(savedInstanceState);
// 启用边到边显示模式
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 初始化DrawerLayout
drawerLayout = findViewById(R.id.drawer_layout);
if (drawerLayout != null) {
if (binding.drawerLayout != null) {
// 设置侧栏在左侧
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
// 设置监听器:侧栏关闭时更新状态
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
binding.drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
// 侧栏滑动时
@ -74,7 +75,7 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O
}
// 设置窗口边距监听器,自动适配系统栏
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_content), (v, insets) -> {
ViewCompat.setOnApplyWindowInsetsListener(binding.mainContent, (v, insets) -> {
// 获取系统栏边距
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置视图内边距以适配系统栏
@ -152,11 +153,14 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O
*
*/
private void closeSidebar() {
if (drawerLayout != null) {
drawerLayout.closeDrawer(Gravity.LEFT);
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(Gravity.LEFT);
}
}
}
//test
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}

@ -739,13 +739,15 @@ public class NotesRepository {
return;
}
String selection = "(" + NoteColumns.TYPE + " = ?) AND (" +
String selection = "(" + NoteColumns.TYPE + " <> ?) AND (" +
NoteColumns.TITLE + " LIKE ? OR " +
NoteColumns.SNIPPET + " LIKE ? OR " +
NoteColumns.ID + " IN (SELECT " + DataColumns.NOTE_ID +
" FROM data WHERE " + DataColumns.CONTENT + " LIKE ?))";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_NOTE),
String.valueOf(Notes.TYPE_SYSTEM),
"%" + keyword + "%",
"%" + keyword + "%",
"%" + keyword + "%"
};

@ -1,160 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Task Google Tasks
* Google Tasks
* JSON
* </p>
*/
public class MetaData extends Task {
/**
*
*/
private final static String TAG = MetaData.class.getSimpleName();
/**
* Google Tasks ID
*/
private String mRelatedGid = null;
/**
*
* <p>
* Google Tasks ID JSON
* JSON notes
* </p>
*
* @param gid Google Tasks ID
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 将关联的 GID 添加到元信息中
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);
}
/**
* Google Tasks ID
*
* @return Google Tasks ID null
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
*
* <p>
* notes notes
* </p>
*
* @return notes null true false
*/
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
/**
* JSON
* <p>
* JSON Google Tasks ID
* </p>
*
* @param js JSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
super.setContentByRemoteJSON(js);
if (getNotes() != null) {
try {
// 从 notes 字段中解析元信息 JSON
JSONObject metaInfo = new JSONObject(getNotes().trim());
// 提取关联的 GID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @param js JSON
* @throws IllegalAccessError
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @return
* @throws IllegalAccessError
*/
@Override
public JSONObject getLocalJSONFromContent() {
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
*
* <p>
*
* </p>
*
* @param c
* @return
* @throws IllegalAccessError
*/
@Override
public int getSyncAction(Cursor c) {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -1,245 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* TaskTaskListMetaData
* Google ID
* JSON
* </p>
*/
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;
/**
* Google Tasks ID
*/
private String mGid;
/**
*
*/
private String mName;
/**
*
*/
private long mLastModified;
/**
* true
*/
private boolean mDeleted;
/**
*
* <p>
* GID null 0 false
* </p>
*/
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
/**
* JSON
* <p>
* ID JSON
* </p>
*
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSON
* <p>
* ID JSON
* </p>
*
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* JSON
* <p>
* JSON
* </p>
*
* @param js JSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
* <p>
* JSON
* </p>
*
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSON
* <p>
* JSON
* </p>
*
* @return JSON
*/
public abstract JSONObject getLocalJSONFromContent();
/**
*
* <p>
*
* </p>
*
* @param c
* @return SYNC_ACTION_*
*/
public abstract int getSyncAction(Cursor c);
/**
* Google Tasks ID
*
* @param gid Google Tasks ID
*/
public void setGid(String gid) {
this.mGid = gid;
}
/**
*
*
* @param name
*/
public void setName(String name) {
this.mName = name;
}
/**
*
*
* @param lastModified
*/
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
/**
*
*
* @param deleted true
*/
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
/**
* Google Tasks ID
*
* @return Google Tasks ID null
*/
public String getGid() {
return this.mGid;
}
/**
*
*
* @return
*/
public String getName() {
return this.mName;
}
/**
*
*
* @return
*/
public long getLastModified() {
return this.mLastModified;
}
/**
*
*
* @return true
*/
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -1,269 +0,0 @@
/*
* 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;
/**
* SQLite
* <p>
*
* MIME
* JSON JSON Google Tasks
* </p>
*/
public class SqlData {
private static final String TAG = SqlData.class.getSimpleName();
/** 无效 ID 标识符 */
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
};
/** ID 字段在投影数组中的索引 */
public static final int DATA_ID_COLUMN = 0;
/** MIME 类型字段在投影数组中的索引 */
public static final int DATA_MIME_TYPE_COLUMN = 1;
/** 内容字段在投影数组中的索引 */
public static final int DATA_CONTENT_COLUMN = 2;
/** 扩展数据 1 字段在投影数组中的索引 */
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
/** 扩展数据 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;
/**
*
* <p>
*
* commit
* </p>
*
* @param context ContentResolver
*/
public SqlData(Context context) {
mContentResolver = context.getContentResolver();
mIsCreate = true;
mDataId = INVALID_ID;
mDataMimeType = DataConstants.NOTE;
mDataContent = "";
mDataContentData1 = 0;
mDataContentData3 = "";
mDiffDataValues = new ContentValues();
}
/**
*
* <p>
*
* commit
* </p>
*
* @param context
* @param c
*/
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDiffDataValues = new ContentValues();
}
/**
*
* <p>
*
* </p>
*
* @param c
*/
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);
}
/**
* JSON
* <p>
* JSON
*
* </p>
*
* @param js JSON
* @throws JSONException JSON
*/
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;
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @return JSON null
* @throws JSONException JSON
*/
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;
}
/**
*
* <p>
*
* - ID
* -
* </p>
*
* @param noteId ID
* @param validateVersion true
* @param version
* @throws ActionFailureException
*/
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;
}
/**
* ID
*
* @return ID
*/
public long getId() {
return mDataId;
}
}

@ -1,668 +0,0 @@
/*
* 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;
/**
* SQLite
* <p>
*
* Google Tasks JSON JSON
*
* </p>
*/
public class SqlNote {
private static final String TAG = SqlNote.class.getSimpleName();
/** 无效 ID 标识符 */
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
};
/** ID 字段在投影数组中的索引 */
public static final int ID_COLUMN = 0;
/** 提醒日期字段在投影数组中的索引 */
public static final int ALERTED_DATE_COLUMN = 1;
/** 背景颜色 ID 字段在投影数组中的索引 */
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;
/** 父文件夹 ID 字段在投影数组中的索引 */
public static final int PARENT_ID_COLUMN = 7;
/** 摘要文本字段在投影数组中的索引 */
public static final int SNIPPET_COLUMN = 8;
/** 笔记类型字段在投影数组中的索引 */
public static final int TYPE_COLUMN = 9;
/** Widget ID 字段在投影数组中的索引 */
public static final int WIDGET_ID_COLUMN = 10;
/** Widget 类型字段在投影数组中的索引 */
public static final int WIDGET_TYPE_COLUMN = 11;
/** 同步 ID 字段在投影数组中的索引 */
public static final int SYNC_ID_COLUMN = 12;
/** 本地修改标记字段在投影数组中的索引 */
public static final int LOCAL_MODIFIED_COLUMN = 13;
/** 原始父文件夹 ID 字段在投影数组中的索引 */
public static final int ORIGIN_PARENT_ID_COLUMN = 14;
/** Google Tasks ID 字段在投影数组中的索引 */
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<SqlData> mDataList;
/**
*
* <p>
*
* commit
* </p>
*
* @param context ContentResolver
*/
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<SqlData>();
}
/**
*
* <p>
*
* commit
* </p>
*
* @param context
* @param c
*/
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
/**
* ID
* <p>
* ID
* commit
* </p>
*
* @param context
* @param id ID
*/
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(id);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
/**
* ID
* <p>
* ID loadFromCursor(Cursor)
* </p>
*
* @param id ID
*/
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();
}
}
/**
*
* <p>
*
* </p>
*
* @param c
*/
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);
}
/**
*
* <p>
* Data SqlData
*
* </p>
*/
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();
}
}
/**
* JSON
* <p>
* JSON
*
*
* </p>
*
* @param js JSON
* @return true false
*/
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;
}
/**
* JSON
* <p>
* JSON
* JSON
* </p>
*
* @return JSON null
*/
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;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID
*/
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
/**
* Google Tasks ID
* <p>
* Google Tasks ID
* </p>
*
* @param gid Google Tasks ID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* ID
* <p>
*
* </p>
*
* @param syncId
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
*
* <p>
* 0
* </p>
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
/**
* ID
*
* @return ID INVALID_ID
*/
public long getId() {
return mId;
}
/**
* ID
*
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
*
* @return true false
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
*
* <p>
*
* - ID
* -
* -
* </p>
*
* @param validateVersion true
* @throws ActionFailureException
* @throws IllegalStateException ID
*/
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);
}
}
}
// 从数据库重新加载最新数据,确保内存状态与数据库一致
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues.clear();
mIsCreate = false;
}
}

@ -1,499 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Node Google Tasks
*
* JSON
* </p>
*/
public class Task extends Node {
/**
*
*/
private static final String TAG = Task.class.getSimpleName();
/**
* true
*/
private boolean mCompleted;
/**
*
*/
private String mNotes;
/**
* JSON
*/
private JSONObject mMetaInfo;
/**
*
*/
private Task mPriorSibling;
/**
*
*/
private TaskList mParent;
/**
*
* <p>
* null
* </p>
*/
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
/**
* JSON
* <p>
* JSON ID
* </p>
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
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;
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
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;
}
/**
* JSON
* <p>
* JSON ID
* </p>
*
* @param js JSON
* @throws ActionFailureException JSON
*/
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");
}
}
}
/**
* JSON
* <p>
* JSON
*
* </p>
*
* @param js JSON
*/
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();
}
}
/**
* JSON
* <p>
* JSON
* JSON
* </p>
*
* @return JSON null
*/
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;
}
}
/**
*
* <p>
* JSON
*
* </p>
*
* @param metaData
*/
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;
}
}
}
/**
*
* <p>
*
*
* </p>
*
* @param c
* @return SYNC_ACTION_*
*/
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;
}
/**
*
* <p>
*
* </p>
*
* @return true false
*/
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
/**
*
*
* @param completed true
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
/**
*
*
* @param notes
*/
public void setNotes(String notes) {
this.mNotes = notes;
}
/**
*
*
* @param priorSibling
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
/**
*
*
* @param parent
*/
public void setParent(TaskList parent) {
this.mParent = parent;
}
/**
*
*
* @return true
*/
public boolean getCompleted() {
return this.mCompleted;
}
/**
*
*
* @return
*/
public String getNotes() {
return this.mNotes;
}
/**
*
*
* @return
*/
public Task getPriorSibling() {
return this.mPriorSibling;
}
/**
*
*
* @return
*/
public TaskList getParent() {
return this.mParent;
}
}

@ -1,510 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Node Google Tasks
*
* JSON
* </p>
*/
public class TaskList extends Node {
/**
*
*/
private static final String TAG = TaskList.class.getSimpleName();
/**
*
*/
private int mIndex;
/**
*
*/
private ArrayList<Task> mChildren;
/**
*
* <p>
* 1
* </p>
*/
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
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;
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
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;
}
/**
* JSON
* <p>
* JSON
* </p>
*
* @param js JSON
* @throws ActionFailureException JSON
*/
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");
}
}
}
/**
* JSON
* <p>
* JSON
*
* </p>
*
* @param js JSON
*/
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) {
// 系统文件夹,根据 ID 设置对应的名称
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();
}
}
/**
* JSON
* <p>
* JSON
*
* </p>
*
* @return JSON null
*/
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;
}
}
/**
*
* <p>
*
*
* </p>
*
* @param c
* @return SYNC_ACTION_*
*/
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;
}
/**
*
*
* @return
*/
public int getChildTaskCount() {
return mChildren.size();
}
/**
*
* <p>
*
* </p>
*
* @param task
* @return true false
*/
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;
}
/**
*
* <p>
*
* </p>
*
* @param task
* @param index 0
* @return true false
*/
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;
}
/**
*
* <p>
*
*
* </p>
*
* @param task
* @return true false
*/
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;
}
/**
*
* <p>
*
* </p>
*
* @param task
* @param index 0 1
* @return true false
*/
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));
}
/**
* GID
*
* @param gid Google Tasks ID
* @return null
*/
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;
}
/**
*
*
* @param task
* @return -1
*/
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
/**
*
*
* @param index 0 1
* @return null
*/
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
}
return mChildren.get(index);
}
/**
* GID
*
* @param gid Google Tasks ID
* @return null
*/
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
return task;
}
return null;
}
/**
*
*
* @return
*/
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
/**
*
*
* @param index
*/
public void setIndex(int index) {
this.mIndex = index;
}
/**
*
*
* @return
*/
public int getIndex() {
return this.mIndex;
}
}

@ -1,55 +0,0 @@
/*
* 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;
/**
*
* <p>
* Google Tasks
*
* RuntimeException
* </p>
*/
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
/**
*
*/
public ActionFailureException() {
super();
}
/**
*
*
* @param paramString
*/
public ActionFailureException(String paramString) {
super(paramString);
}
/**
*
*
* @param paramString
* @param paramThrowable
*/
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -1,55 +0,0 @@
/*
* 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;
/**
*
* <p>
* Google Tasks
* 访 Google Tasks
* Exception
* </p>
*/
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
/**
*
*/
public NetworkFailureException() {
super();
}
/**
*
*
* @param paramString
*/
public NetworkFailureException(String paramString) {
super(paramString);
}
/**
*
*
* @param paramString
* @param paramThrowable
*/
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -1,222 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* AsyncTask Google Tasks
*
* </p>
*/
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
/** 同步通知的唯一标识符 */
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
/**
*
* <p>
*
* </p>
*/
public interface OnCompleteListener {
/**
*
*/
void onComplete();
}
/** 应用上下文 */
private Context mContext;
/** 通知管理器 */
private NotificationManager mNotifiManager;
/** Google Tasks 管理器实例 */
private GTaskManager mTaskManager;
/** 同步完成监听器 */
private OnCompleteListener mOnCompleteListener;
/**
*
* <p>
*
* </p>
*
* @param context
* @param listener
*/
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
// 获取系统通知服务
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
// 获取 GTaskManager 单例
mTaskManager = GTaskManager.getInstance();
}
/**
*
* <p>
* GTaskManager cancelSync()
* </p>
*/
public void cancelSync() {
mTaskManager.cancelSync();
}
/**
*
* <p>
* AsyncTask publishProgress() UI 线
* </p>
*
* @param message
*/
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
/**
*
* <p>
*
*
* </p>
*
* @param tickerId ID
* @param content
*/
private void showNotification(int tickerId, String content) {
PendingIntent pendingIntent;
// 根据同步结果选择跳转目标
if (tickerId != R.string.ticker_success) {
// 同步失败或取消,跳转到设置页面
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
} else {
// 同步成功,跳转到笔记列表
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
// 构建通知
Notification.Builder builder = new Notification.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setOngoing(true);
Notification notification=builder.getNotification();
// 显示通知
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
/**
*
* <p>
* 线 Google Tasks
* </p>
*
* @param unused 使
* @return GTaskManager.STATE_SUCCESSSTATE_NETWORK_ERRORSTATE_INTERNAL_ERRORSTATE_SYNC_IN_PROGRESS STATE_SYNC_CANCELLED
*/
@Override
protected Integer doInBackground(Void... unused) {
// 发布登录进度
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
// 执行同步并返回结果
return mTaskManager.sync(mContext, this);
}
/**
*
* <p>
* UI 线广
* </p>
*
* @param progress
*/
@Override
protected void onProgressUpdate(String... progress) {
// 显示进度通知
showNotification(R.string.ticker_syncing, progress[0]);
// 如果上下文是 GTaskSyncService发送广播
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
/**
*
* <p>
*
*
* </p>
*
* @param result
*/
@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();
}
}
}

@ -1,784 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Google Tasks API Google Tasks
*
* 使 HTTP Google Tasks API Cookie
* </p>
*/
public class GTaskClient {
private static final String TAG = GTaskClient.class.getSimpleName();
/** Google Tasks 基础 URL */
private static final String GTASK_URL = "https://mail.google.com/tasks/";
/** Google Tasks GET 请求 URL */
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
/** Google Tasks POST 请求 URL */
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;
/**
*
* <p>
*
* </p>
*/
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
/**
* GTaskClient
* <p>
* 使线
* </p>
*
* @return GTaskClient
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* Google Tasks
* <p>
*
* Cookie 5
* Gmail/Googlemail
* </p>
*
* @param activity Activity
* @return true false
*/
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;
}
/**
* Google
* <p>
* Google
* invalidateToken true使
* </p>
*
* @param activity Activity
* @param invalidateToken 使
* @return null
*/
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<Bundle> 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;
}
/**
* Google Tasks
* <p>
* 使 Google Tasks使
* </p>
*
* @param activity Activity
* @param authToken
* @return true false
*/
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;
}
/**
* 使 Google Tasks
* <p>
* Google Tasks GET Cookie
* </p>
*
* @param authToken
* @return true false
*/
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<Cookie> 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 = ")}</script>";
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;
}
/**
* ID
* <p>
* ID
* </p>
*
* @return ID
*/
private int getActionId() {
return mActionId++;
}
/**
* HTTP POST
* <p>
* application/x-www-form-urlencoded
* </p>
*
* @return HttpPost
*/
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;
}
/**
* HTTP
* <p>
* HTTP gzip deflate
* </p>
*
* @param entity HTTP
* @return
* @throws IOException
*/
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();
}
}
/**
* POST Google Tasks
* <p>
* JSON POST JSON
* </p>
*
* @param js JSON
* @return JSON
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
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");
}
}
/**
*
* <p>
* Google Tasks ID
* </p>
*
* @param task
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
/**
*
* <p>
* Google Tasks ID
* </p>
*
* @param tasklist
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
/**
*
* <p>
* Google Tasks
*
* </p>
*
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
}
/**
*
* <p>
* 10
* </p>
*
* @param node null
* @throws NetworkFailureException
*/
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()));
}
}
/**
*
* <p>
*
* </p>
*
* @param task
* @param preParent
* @param curParent
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
/**
*
* <p>
* Google Tasks
* </p>
*
* @param node
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
/**
*
* <p>
* Google Tasks
* </p>
*
* @return JSON
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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 = ")}</script>";
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");
}
}
/**
*
* <p>
* Google Tasks
* </p>
*
* @param listGid Google ID
* @return JSON
* @throws NetworkFailureException
* @throws ActionFailureException JSON
*/
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");
}
}
/**
*
*
* @return Google
*/
public Account getSyncAccount() {
return mAccount;
}
/**
*
* <p>
*
* </p>
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}

@ -1,857 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Google Tasks
*
*
* </p>
*/
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<String, TaskList> mGTaskListHashMap;
private HashMap<String, Node> mGTaskHashMap;
private HashMap<String, MetaData> mMetaHashMap;
private TaskList mMetaList;
private HashSet<Long> mLocalDeleteIdMap;
private HashMap<String, Long> mGidToNid;
private HashMap<Long, String> mNidToGid;
/**
*
* <p>
*
* </p>
*/
private GTaskManager() {
mSyncing = false;
mCancelled = false;
mGTaskListHashMap = new HashMap<String, TaskList>();
mGTaskHashMap = new HashMap<String, Node>();
mMetaHashMap = new HashMap<String, MetaData>();
mMetaList = null;
mLocalDeleteIdMap = new HashSet<Long>();
mGidToNid = new HashMap<String, Long>();
mNidToGid = new HashMap<Long, String>();
}
/**
* GTaskManager
* <p>
* 使线
* </p>
*
* @return GTaskManager
*/
public static synchronized GTaskManager getInstance() {
if (mInstance == null) {
mInstance = new GTaskManager();
}
return mInstance;
}
/**
* Activity
* <p>
* Google
* </p>
*
* @param activity Activity
*/
public synchronized void setActivityContext(Activity activity) {
// used for getting authtoken
mActivity = activity;
}
/**
*
* <p>
* Google Tasks
* Google Tasks
* </p>
*
* @param context
* @param asyncTask
* @return STATE_SUCCESSSTATE_NETWORK_ERRORSTATE_INTERNAL_ERRORSTATE_SYNC_IN_PROGRESS STATE_SYNC_CANCELLED
*/
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<Map.Entry<String, Node>> iter = mGTaskHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, Node> 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<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> 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<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> 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;
}
}
}
/**
*
*
* @return Google
*/
public String getSyncAccount() {
return mActivity == null ? null : GTaskClient.getInstance().getSyncAccount().name;
}
/**
*
* <p>
*
* </p>
*/
public void cancelSync() {
mCancelled = true;
}
}

@ -1,241 +0,0 @@
/*
* 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;
/**
* Google Tasks
* <p>
* Google Tasks
* 广
* </p>
*/
public class GTaskSyncService extends Service {
/** Intent 附加参数名称,用于指定同步操作类型 */
public final static String ACTION_STRING_NAME = "sync_action_type";
/** 启动同步操作的 Action 值 */
public final static int ACTION_START_SYNC = 0;
/** 取消同步操作的 Action 值 */
public final static int ACTION_CANCEL_SYNC = 1;
/** 无效的 Action 值 */
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 = "";
/**
*
* <p>
* GTaskASyncTask
* 广
* </p>
*/
private void startSync() {
// 检查是否已有同步任务在运行
if (mSyncTask == null) {
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
public void onComplete() {
// 清空同步任务引用
mSyncTask = null;
// 发送同步完成广播
sendBroadcast("");
// 停止服务
stopSelf();
}
});
// 发送同步开始广播
sendBroadcast("");
// 执行异步同步任务
mSyncTask.execute();
}
}
/**
*
* <p>
* cancelSync()
* </p>
*/
private void cancelSync() {
if (mSyncTask != null) {
// 取消异步同步任务
mSyncTask.cancelSync();
}
}
/**
*
* <p>
* null
* </p>
*/
@Override
public void onCreate() {
mSyncTask = null;
}
/**
*
* <p>
* Intent Action
* ACTION_START_SYNC ACTION_CANCEL_SYNC
* </p>
*
* @param intent Intent Action
* @param flags
* @param startId ID
* @return START_STICKY
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
// 检查 Intent 是否包含 Action 参数
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
// 根据 Action 类型执行相应操作
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);
}
/**
*
* <p>
*
* </p>
*/
@Override
public void onLowMemory() {
if (mSyncTask != null) {
// 取消同步任务以释放内存
mSyncTask.cancelSync();
}
}
/**
*
* <p>
* null
* </p>
*
* @param intent Intent
* @return null
*/
public IBinder onBind(Intent intent) {
return null;
}
/**
* 广
* <p>
* 广
* </p>
*
* @param msg
*/
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);
}
/**
*
* <p>
* Activity GTaskManager
* </p>
*
* @param activity Activity Google
*/
public static void startSync(Activity 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);
}
/**
*
* <p>
*
* </p>
*
* @param context
*/
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);
}
/**
*
*
* @return true false
*/
public static boolean isSyncing() {
return mSyncTask != null;
}
/**
*
*
* @return
*/
public static String getProgressString() {
return mSyncProgress;
}
}

@ -0,0 +1,67 @@
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
public class SearchHistoryManager {
private static final String PREF_NAME = "search_history";
private static final String KEY_HISTORY = "history_list";
private static final int MAX_HISTORY_SIZE = 10;
private final SharedPreferences mPrefs;
public SearchHistoryManager(Context context) {
mPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public List<String> getHistory() {
String json = mPrefs.getString(KEY_HISTORY, "");
List<String> list = new ArrayList<>();
if (TextUtils.isEmpty(json)) {
return list;
}
try {
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
list.add(array.getString(i));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
public void addHistory(String keyword) {
if (TextUtils.isEmpty(keyword)) return;
List<String> history = getHistory();
// Remove existing to move to top
history.remove(keyword);
history.add(0, keyword);
// Limit size
if (history.size() > MAX_HISTORY_SIZE) {
history = history.subList(0, MAX_HISTORY_SIZE);
}
saveHistory(history);
}
public void removeHistory(String keyword) {
List<String> history = getHistory();
if (history.remove(keyword)) {
saveHistory(history);
}
}
public void clearHistory() {
mPrefs.edit().remove(KEY_HISTORY).apply();
}
private void saveHistory(List<String> history) {
JSONArray array = new JSONArray(history);
mPrefs.edit().putString(KEY_HISTORY, array.toString()).apply();
}
}

@ -20,10 +20,11 @@ import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import net.micode.notes.databinding.DatetimePickerBinding;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
@ -72,6 +73,7 @@ public class DateTimePicker extends FrameLayout {
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
private final DatetimePickerBinding binding;
private Calendar mDate;
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
@ -281,19 +283,19 @@ public class DateTimePicker extends FrameLayout {
// 判断当前是否为下午
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 加载布局
inflate(context, R.layout.datetime_picker, this);
binding = DatetimePickerBinding.inflate(LayoutInflater.from(context), this, true);
// 初始化日期选择器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner = binding.date;
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner = binding.hour;
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner = binding.minute;
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
@ -301,7 +303,7 @@ public class DateTimePicker extends FrameLayout {
// 初始化上午/下午选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner = binding.amPm;
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);

@ -76,6 +76,8 @@ import java.util.regex.Pattern;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.appbar.MaterialToolbar;
import net.micode.notes.databinding.NoteEditBinding;
public class NoteEditActivity extends AppCompatActivity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
@ -166,19 +168,61 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private String mUserQuery;
private Pattern mPattern;
private NoteEditBinding binding;
/**
* ID
*/
private View getBgSelectorView(int viewId) {
switch (viewId) {
case R.id.iv_bg_yellow_select:
return binding.ivBgYellowSelect;
case R.id.iv_bg_red_select:
return binding.ivBgRedSelect;
case R.id.iv_bg_blue_select:
return binding.ivBgBlueSelect;
case R.id.iv_bg_green_select:
return binding.ivBgGreenSelect;
case R.id.iv_bg_white_select:
return binding.ivBgWhiteSelect;
default:
throw new IllegalArgumentException("Unknown view ID: " + viewId);
}
}
/**
* ID
*/
private View getFontSelectorView(int viewId) {
switch (viewId) {
case R.id.iv_small_select:
return binding.ivSmallSelect;
case R.id.iv_medium_select:
return binding.ivMediumSelect;
case R.id.iv_large_select:
return binding.ivLargeSelect;
case R.id.iv_super_select:
return binding.ivSuperSelect;
default:
throw new IllegalArgumentException("Unknown view ID: " + viewId);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit);
// 使用ViewBinding设置布局
binding = NoteEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 初始化Toolbar使用MaterialToolbar与列表页面一致
MaterialToolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
setSupportActionBar(binding.toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}
toolbar.setNavigationOnClickListener(v -> finish());
binding.toolbar.setNavigationOnClickListener(v -> finish());
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
@ -311,19 +355,19 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* </p>
*/
private void initResources() {
mHeadViewPanel = findViewById(R.id.note_title);
mHeadViewPanel = binding.noteTitle;
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = findViewById(R.id.tv_modified_date);
mNoteHeaderHolder.ivAlertIcon = findViewById(R.id.iv_alert_icon);
mNoteHeaderHolder.tvAlertDate = findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.tvModified = binding.tvModifiedDate;
mNoteHeaderHolder.ivAlertIcon = binding.ivAlertIcon;
mNoteHeaderHolder.tvAlertDate = binding.tvAlertDate;
mNoteHeaderHolder.ibSetBgColor = binding.btnSetBgColor;
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteHeaderHolder.tvCharCount = findViewById(R.id.tv_char_count);
mNoteHeaderHolder.etTitle = findViewById(R.id.et_title);
mNoteHeaderHolder.tvCharCount = binding.tvCharCount;
mNoteHeaderHolder.etTitle = binding.etTitle;
mNoteEditor = findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
mNoteEditor = binding.noteEditView;
mNoteEditorPanel = binding.svNoteEdit;
mNoteBgColorSelector = binding.noteBgColorSelector;
mNoteEditor.addTextChangedListener(new TextWatcher() {
@Override
@ -364,13 +408,48 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
// 设置背景颜色选择器的点击事件
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv = findViewById(id);
ImageView iv;
switch (id) {
case R.id.iv_bg_yellow:
iv = binding.ivBgYellow;
break;
case R.id.iv_bg_red:
iv = binding.ivBgRed;
break;
case R.id.iv_bg_blue:
iv = binding.ivBgBlue;
break;
case R.id.iv_bg_green:
iv = binding.ivBgGreen;
break;
case R.id.iv_bg_white:
iv = binding.ivBgWhite;
break;
default:
throw new IllegalArgumentException("Unknown view ID: " + id);
}
iv.setOnClickListener(this);
}
mFontSizeSelector = findViewById(R.id.font_size_selector);
mFontSizeSelector = binding.fontSizeSelector;
for (int id : sFontSizeBtnsMap.keySet()) {
View view = findViewById(id);
View view;
switch (id) {
case R.id.ll_font_small:
view = binding.llFontSmall;
break;
case R.id.ll_font_normal:
view = binding.llFontNormal;
break;
case R.id.ll_font_large:
view = binding.llFontLarge;
break;
case R.id.ll_font_super:
view = binding.llFontSuper;
break;
default:
throw new IllegalArgumentException("Unknown view ID: " + id);
}
view.setOnClickListener(this);
}
@ -384,7 +463,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
if (mFontSizeId >= TextAppearanceResources.getResourcesSize()) {
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = findViewById(R.id.note_edit_list);
mEditTextList = binding.noteEditList;
}
@Override
@ -416,7 +495,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
}
mNoteHeaderHolder.etTitle.setText(mWorkingNote.getTitle());
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
View view = getBgSelectorView(sBgSelectorSelectionMap.get(id));
if (view != null) {
view.setVisibility(View.GONE);
}
}
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
@ -549,6 +631,12 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
clearSettingState();
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
/**
*
* <p>
@ -590,18 +678,28 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
}
} else if (sBgSelectorBtnsMap.containsKey(id)) {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.GONE);
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.GONE);
}
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
} else if (sFontSizeBtnsMap.containsKey(id)) {
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.GONE);
View fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId));
if (fontView != null) {
fontView.setVisibility(View.GONE);
}
mFontSizeId = sFontSizeBtnsMap.get(id);
mSharedPrefs.edit().putInt(PREFERENCE_FONT_SIZE, mFontSizeId).commit();
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId));
if (fontView != null) {
fontView.setVisibility(View.VISIBLE);
}
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
getWorkingText();
switchToListMode(mWorkingNote.getContent());
@ -660,8 +758,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* </p>
*/
public void onBackgroundColorChanged() {
findViewById(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId())).setVisibility(
View.VISIBLE);
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
}
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
}
@ -745,7 +845,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
break;
case R.id.menu_font_size:
mFontSizeSelector.setVisibility(View.VISIBLE);
findViewById(sFontSelectorSelectionMap.get(mFontSizeId)).setVisibility(View.VISIBLE);
View fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId));
if (fontView != null) {
fontView.setVisibility(View.VISIBLE);
}
break;
case R.id.menu_list_mode:
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?

@ -0,0 +1,188 @@
package net.micode.notes.ui;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.SearchHistoryManager;
import java.util.ArrayList;
import java.util.List;
public class NoteSearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener {
private SearchView mSearchView;
private RecyclerView mRecyclerView;
private TextView mTvNoResult;
private NoteSearchAdapter mAdapter;
private NotesRepository mRepository;
private SearchHistoryManager mHistoryManager;
private TextView mBtnShowHistory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_search);
mRepository = new NotesRepository(getContentResolver());
mHistoryManager = new SearchHistoryManager(this);
initViews();
// Initial state: search is empty, show history button if there is history, or just show list
// Requirement: "history option below search bar"
showHistoryOption();
}
private void initViews() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
toolbar.setNavigationOnClickListener(v -> finish());
mSearchView = findViewById(R.id.search_view);
mSearchView.setOnQueryTextListener(this);
mSearchView.setFocusable(true);
mSearchView.setIconified(false);
mSearchView.requestFocusFromTouch();
mBtnShowHistory = findViewById(R.id.btn_show_history);
mBtnShowHistory.setOnClickListener(v -> showHistoryList());
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NoteSearchAdapter(this, this);
mRecyclerView.setAdapter(mAdapter);
mTvNoResult = findViewById(R.id.tv_no_result);
}
private void showHistoryOption() {
// Show the "History" button, hide the list
mBtnShowHistory.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
mTvNoResult.setVisibility(View.GONE);
}
private void showHistoryList() {
List<String> history = mHistoryManager.getHistory();
if (history.isEmpty()) {
// If no history, maybe show a toast or empty state?
// But for now, let's just show the empty list which is fine
}
List<Object> data = new ArrayList<>(history);
mAdapter.setData(data, null);
mBtnShowHistory.setVisibility(View.GONE); // Hide button when showing list
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
private void performSearch(String query) {
if (TextUtils.isEmpty(query)) {
showHistoryOption();
return;
}
// Hide history button when searching
mBtnShowHistory.setVisibility(View.GONE);
mRepository.searchNotes(query, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> result) {
runOnUiThread(() -> {
List<Object> data = new ArrayList<>(result);
mAdapter.setData(data, query);
if (data.isEmpty()) {
mTvNoResult.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NoteSearchActivity.this, "Search failed: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
}
@Override
public boolean onQueryTextSubmit(String query) {
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
performSearch(query);
mSearchView.clearFocus(); // Hide keyboard
}
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
if (TextUtils.isEmpty(newText)) {
showHistoryOption();
} else {
performSearch(newText);
}
return true;
}
@Override
public void onNoteClick(NotesRepository.NoteInfo note) {
// Save history when user clicks a result
String query = mSearchView.getQuery().toString();
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
}
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, note.getId());
// Pass search keyword for highlighting in editor
// NoteEditActivity uses SearchManager.EXTRA_DATA_KEY for ID and USER_QUERY for keyword
intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId()));
intent.putExtra(android.app.SearchManager.USER_QUERY, mSearchView.getQuery().toString());
startActivity(intent);
}
@Override
public void onHistoryClick(String keyword) {
mSearchView.setQuery(keyword, true);
}
@Override
public void onHistoryDelete(String keyword) {
mHistoryManager.removeHistory(keyword);
// Refresh history view if we are currently showing history (search box is empty)
if (TextUtils.isEmpty(mSearchView.getQuery())) {
showHistoryList();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRepository != null) {
mRepository.shutdown();
}
}
}

@ -0,0 +1,180 @@
package net.micode.notes.ui;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NoteSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HISTORY = 1;
private static final int TYPE_NOTE = 2;
private Context mContext;
private List<Object> mDataList;
private String mSearchKeyword;
private OnItemClickListener mListener;
public interface OnItemClickListener {
void onNoteClick(NotesRepository.NoteInfo note);
void onHistoryClick(String keyword);
void onHistoryDelete(String keyword);
}
public NoteSearchAdapter(Context context, OnItemClickListener listener) {
mContext = context;
mListener = listener;
mDataList = new ArrayList<>();
}
public void setData(List<Object> data, String keyword) {
mDataList = data;
mSearchKeyword = keyword;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
Object item = mDataList.get(position);
if (item instanceof String) {
return TYPE_HISTORY;
} else if (item instanceof NotesRepository.NoteInfo) {
return TYPE_NOTE;
}
return super.getItemViewType(position);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == TYPE_HISTORY) {
View view = LayoutInflater.from(mContext).inflate(R.layout.search_history_item, parent, false);
return new HistoryViewHolder(view);
} else {
View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false);
return new NoteViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HistoryViewHolder) {
String keyword = (String) mDataList.get(position);
((HistoryViewHolder) holder).bind(keyword);
} else if (holder instanceof NoteViewHolder) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) mDataList.get(position);
((NoteViewHolder) holder).bind(note);
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
class HistoryViewHolder extends RecyclerView.ViewHolder {
TextView tvKeyword;
ImageView ivDelete;
public HistoryViewHolder(View itemView) {
super(itemView);
tvKeyword = itemView.findViewById(R.id.tv_history_keyword);
ivDelete = itemView.findViewById(R.id.iv_delete_history);
}
public void bind(final String keyword) {
tvKeyword.setText(keyword);
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryClick(keyword);
});
ivDelete.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryDelete(keyword);
});
}
}
class NoteViewHolder extends RecyclerView.ViewHolder {
ImageView ivTypeIcon;
TextView tvTitle;
TextView tvTime;
TextView tvName;
ImageView ivAlertIcon;
CheckBox checkbox;
public NoteViewHolder(View itemView) {
super(itemView);
ivTypeIcon = itemView.findViewById(R.id.iv_type_icon);
tvTitle = itemView.findViewById(R.id.tv_title);
tvTime = itemView.findViewById(R.id.tv_time);
tvName = itemView.findViewById(R.id.tv_name);
ivAlertIcon = itemView.findViewById(R.id.iv_alert_icon);
checkbox = itemView.findViewById(android.R.id.checkbox);
}
public void bind(final NotesRepository.NoteInfo note) {
// 设置标题和高亮
// NoteInfo.title defaults to snippet if title is empty, so it's safe to use title
if (!TextUtils.isEmpty(mSearchKeyword)) {
tvTitle.setText(getHighlightText(note.title, mSearchKeyword));
} else {
tvTitle.setText(note.title);
}
// 设置时间
tvTime.setText(android.text.format.DateUtils.getRelativeTimeSpanString(note.modifiedDate));
// 设置背景(如果 NoteInfo 中有背景ID
// 注意NoteInfo 中 bgColorId 是整型ID需要转换为资源ID
// 这里为了简单,暂不设置复杂的背景,或者使用默认背景
// 点击事件
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onNoteClick(note);
});
// 隐藏不需要的视图
ivTypeIcon.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
checkbox.setVisibility(View.GONE);
ivAlertIcon.setVisibility(View.GONE);
}
}
private Spannable getHighlightText(String text, String keyword) {
if (text == null) text = "";
SpannableString spannable = new SpannableString(text);
if (!TextUtils.isEmpty(keyword)) {
Pattern pattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
spannable.setSpan(
new BackgroundColorSpan(0x40FFFF00), // 半透明黄色
matcher.start(),
matcher.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
return spannable;
}
}

@ -29,8 +29,6 @@ import androidx.appcompat.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.Button;
@ -42,10 +40,8 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
@ -54,6 +50,7 @@ import androidx.lifecycle.ViewModelProvider;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.databinding.NoteListBinding;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.NoteInfoAdapter;
import net.micode.notes.viewmodel.NotesListViewModel;
@ -87,13 +84,9 @@ public class NotesListActivity extends AppCompatActivity
private static final int REQUEST_CODE_CHECK_PASSWORD_FOR_LOCK = 105;
private NotesListViewModel viewModel;
private ListView notesListView;
private androidx.appcompat.widget.Toolbar toolbar;
private NoteListBinding binding;
private NoteInfoAdapter adapter;
private DrawerLayout drawerLayout;
private FloatingActionButton fabNewNote;
private LinearLayout breadcrumbContainer;
private LinearLayout breadcrumbItems;
private View sidebarFragment;
// 多选模式状态
private boolean isMultiSelectMode = false;
@ -120,16 +113,12 @@ public class NotesListActivity extends AppCompatActivity
// 启用边缘到边缘显示
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
setContentView(R.layout.note_list);
binding = NoteListBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 处理窗口insets状态栏和导航栏
View mainView = findViewById(android.R.id.content);
ViewCompat.setOnApplyWindowInsetsListener(mainView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置内容区域的padding以避免被状态栏遮挡
v.setPadding(insets.left, insets.top, insets.right, insets.bottom);
return WindowInsetsCompat.CONSUMED;
});
// 注意CoordinatorLayout和AppBarLayout会自动处理WindowInsets
// FAB也会自动避开导航栏
// 不需要手动设置padding
initViewModel();
@ -137,12 +126,12 @@ public class NotesListActivity extends AppCompatActivity
if (savedInstanceState != null) {
mPendingNodeIdToOpen = savedInstanceState.getLong(KEY_PENDING_NODE_ID, -1);
mPendingNodeTypeToOpen = savedInstanceState.getInt(KEY_PENDING_NODE_TYPE, -1);
long savedFolderId = savedInstanceState.getLong(KEY_CURRENT_FOLDER_ID, Notes.ID_ROOT_FOLDER);
if (savedFolderId != Notes.ID_ROOT_FOLDER) {
viewModel.setCurrentFolderId(savedFolderId);
}
Log.d(TAG, "Restored pending node: " + mPendingNodeIdToOpen + ", type: " + mPendingNodeTypeToOpen + ", folder: " + savedFolderId);
}
@ -194,23 +183,20 @@ public class NotesListActivity extends AppCompatActivity
*
*/
private void initViews() {
notesListView = findViewById(R.id.notes_list);
toolbar = findViewById(R.id.toolbar);
drawerLayout = findViewById(R.id.drawer_layout);
// 初始化面包屑导航
breadcrumbContainer = findViewById(R.id.breadcrumb_container);
breadcrumbItems = findViewById(R.id.breadcrumb_items);
// 特殊处理Fragment标签不会在ViewBinding中生成字段
// 必须使用findViewById获取<fragment>标签声明的Fragment实例
// 这是Android ViewBinding的已知限制不是遗漏
sidebarFragment = findViewById(R.id.sidebar_fragment);
// 设置适配器
adapter = new NoteInfoAdapter(this);
notesListView.setAdapter(adapter);
binding.notesList.setAdapter(adapter);
adapter.setOnNoteButtonClickListener(this);
adapter.setOnNoteItemClickListener(this);
adapter.setOnNoteItemLongClickListener(this);
// 设置点击监听
notesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
binding.notesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Object item = parent.getItemAtPosition(position);
@ -222,8 +208,7 @@ public class NotesListActivity extends AppCompatActivity
});
// 初始化 Toolbar
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
setSupportActionBar(binding.toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.app_name);
}
@ -232,16 +217,13 @@ public class NotesListActivity extends AppCompatActivity
updateToolbarForNormalMode();
// 设置 Toolbar 的汉堡菜单按钮点击监听器(打开侧栏)
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
binding.toolbar.setNavigationOnClickListener(v -> {
binding.drawerLayout.openDrawer(sidebarFragment);
});
// Set FAB click event
fabNewNote = findViewById(R.id.btn_new_note);
if (fabNewNote != null) {
fabNewNote.setOnClickListener(v -> {
if (binding.btnNewNote != null) {
binding.btnNewNote.setOnClickListener(v -> {
Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, viewModel.getCurrentFolderId());
@ -334,10 +316,10 @@ public class NotesListActivity extends AppCompatActivity
public void onChanged(Boolean refreshNeeded) {
if (refreshNeeded != null && refreshNeeded) {
// 通知侧栏刷新
SidebarFragment sidebarFragment = (SidebarFragment) getSupportFragmentManager()
SidebarFragment sidebarFrag = (SidebarFragment) getSupportFragmentManager()
.findFragmentById(R.id.sidebar_fragment);
if (sidebarFragment != null) {
sidebarFragment.refreshFolderTree();
if (sidebarFrag != null) {
sidebarFrag.refreshFolderTree();
}
// 重置刷新状态
viewModel.getSidebarRefreshNeeded().setValue(false);
@ -352,11 +334,11 @@ public class NotesListActivity extends AppCompatActivity
* @param path
*/
private void updateBreadcrumb(List<NotesRepository.NoteInfo> path) {
if (breadcrumbItems == null || path == null) {
if (binding.breadcrumbInclude == null || binding.breadcrumbInclude.breadcrumbItems == null || path == null) {
return;
}
breadcrumbItems.removeAllViews();
binding.breadcrumbInclude.breadcrumbItems.removeAllViews();
for (int i = 0; i < path.size(); i++) {
NotesRepository.NoteInfo folder = path.get(i);
@ -367,12 +349,12 @@ public class NotesListActivity extends AppCompatActivity
separator.setText(" > ");
separator.setTextSize(14);
separator.setTextColor(android.R.color.darker_gray);
breadcrumbItems.addView(separator);
binding.breadcrumbInclude.breadcrumbItems.addView(separator);
}
// 创建面包屑项
TextView breadcrumbItem = (TextView) getLayoutInflater()
.inflate(R.layout.breadcrumb_item, breadcrumbItems, false);
.inflate(R.layout.breadcrumb_item, binding.breadcrumbInclude.breadcrumbItems, false);
breadcrumbItem.setText(folder.title);
// 如果是当前文件夹(最后一个),高亮显示且不可点击
@ -385,7 +367,7 @@ public class NotesListActivity extends AppCompatActivity
breadcrumbItem.setOnClickListener(v -> viewModel.enterFolder(targetFolderId));
}
breadcrumbItems.addView(breadcrumbItem);
binding.breadcrumbInclude.breadcrumbItems.addView(breadcrumbItem);
}
}
@ -569,8 +551,8 @@ public class NotesListActivity extends AppCompatActivity
private void enterMultiSelectMode() {
isMultiSelectMode = true;
// 隐藏FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.GONE);
if (binding.btnNewNote != null) {
binding.btnNewNote.setVisibility(View.GONE);
}
// 更新toolbar为多选模式
updateToolbarForMultiSelectMode();
@ -582,8 +564,8 @@ public class NotesListActivity extends AppCompatActivity
private void exitMultiSelectMode() {
isMultiSelectMode = false;
// 显示FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.VISIBLE);
if (binding.btnNewNote != null) {
binding.btnNewNote.setVisibility(View.VISIBLE);
}
// 清除选中状态
viewModel.clearSelection();
@ -599,22 +581,22 @@ public class NotesListActivity extends AppCompatActivity
* Toolbar
*/
private void updateToolbarForMultiSelectMode() {
if (toolbar == null) return;
if (binding.toolbar == null) return;
// 设置标题为选中数量
int selectedCount = viewModel.getSelectedCount();
String title = getString(R.string.menu_select_title, selectedCount);
toolbar.setTitle(title);
binding.toolbar.setTitle(title);
// 设置导航图标为返回(取消多选)
toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
toolbar.setNavigationOnClickListener(v -> exitMultiSelectMode());
binding.toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
binding.toolbar.setNavigationOnClickListener(v -> exitMultiSelectMode());
// 移除普通模式的菜单(如果有)
toolbar.getMenu().clear();
binding.toolbar.getMenu().clear();
// 直接在toolbar上添加操作按钮不在三点菜单中
Menu menu = toolbar.getMenu();
Menu menu = binding.toolbar.getMenu();
// 删除按钮
MenuItem deleteItem = menu.add(Menu.NONE, R.id.multi_select_delete, 1, getString(R.string.menu_delete));
@ -644,36 +626,34 @@ public class NotesListActivity extends AppCompatActivity
* Toolbar
*/
private void updateToolbarForNormalMode() {
if (toolbar == null) return;
if (binding.toolbar == null) return;
// 清除多选模式菜单
toolbar.getMenu().clear();
binding.toolbar.getMenu().clear();
// 设置标题
if (viewModel.isTrashMode()) {
toolbar.setTitle(R.string.menu_trash);
binding.toolbar.setTitle(R.string.menu_trash);
} else {
toolbar.setTitle(R.string.app_name);
binding.toolbar.setTitle(R.string.app_name);
// 添加普通模式菜单
toolbar.inflateMenu(R.menu.note_list);
binding.toolbar.inflateMenu(R.menu.note_list);
}
// 设置导航图标为汉堡菜单
toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size);
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
binding.toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size);
binding.toolbar.setNavigationOnClickListener(v -> {
binding.drawerLayout.openDrawer(sidebarFragment);
});
// 如果是回收站模式,不显示新建按钮
if (viewModel.isTrashMode()) {
if (fabNewNote != null) {
fabNewNote.setVisibility(View.GONE);
if (binding.btnNewNote != null) {
binding.btnNewNote.setVisibility(View.GONE);
}
} else {
if (fabNewNote != null) {
fabNewNote.setVisibility(View.VISIBLE);
if (binding.btnNewNote != null) {
binding.btnNewNote.setVisibility(View.VISIBLE);
}
}
}
@ -766,8 +746,8 @@ public class NotesListActivity extends AppCompatActivity
switch (itemId) {
case R.id.menu_search:
// TODO: 打开搜索对话框
Toast.makeText(this, "搜索功能开发中", Toast.LENGTH_SHORT).show();
Intent searchIntent = new Intent(this, NoteSearchActivity.class);
startActivity(searchIntent);
return true;
case R.id.menu_new_folder:
// 创建新文件夹
@ -834,15 +814,6 @@ public class NotesListActivity extends AppCompatActivity
return super.onContextItemSelected(item);
}
/**
*
*/
@Override
protected void onDestroy() {
super.onDestroy();
// 清理资源
}
private void updateSelectionState(int position, boolean selected) {
Log.d("NotesListActivity", "===== updateSelectionState called =====");
Log.d("NotesListActivity", "position: " + position + ", selected: " + selected);
@ -878,8 +849,8 @@ public class NotesListActivity extends AppCompatActivity
// 跳转到指定文件夹
viewModel.enterFolder(folderId);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(sidebarFragment);
}
}
@ -888,8 +859,8 @@ public class NotesListActivity extends AppCompatActivity
// 跳转到回收站
viewModel.enterFolder(Notes.ID_TRASH_FOLER);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(sidebarFragment);
}
}
@ -917,11 +888,11 @@ public class NotesListActivity extends AppCompatActivity
@Override
public void onSettingsSelected() {
// 打开设置页面
Intent intent = new Intent(this, NotesPreferenceActivity.class);
Intent intent = new Intent(this, net.micode.notes.ui.NotesPreferenceActivity.class);
startActivity(intent);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(sidebarFragment);
}
}
@ -988,8 +959,8 @@ public class NotesListActivity extends AppCompatActivity
@Override
public void onCloseSidebar() {
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(sidebarFragment);
}
}
@ -1006,9 +977,9 @@ public class NotesListActivity extends AppCompatActivity
if (isMultiSelectMode) {
// 多选模式:退出多选模式
exitMultiSelectMode();
} else if (drawerLayout != null && drawerLayout.isDrawerOpen(findViewById(R.id.sidebar_fragment))) {
} else if (binding.drawerLayout != null && binding.drawerLayout.isDrawerOpen(sidebarFragment)) {
// 侧栏打开:关闭侧栏
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
binding.drawerLayout.closeDrawer(sidebarFragment);
} else if (viewModel.getCurrentFolderId() != Notes.ID_ROOT_FOLDER &&
viewModel.getCurrentFolderId() != Notes.ID_CALL_RECORD_FOLDER) {
// 子文件夹:返回上一级
@ -1021,4 +992,10 @@ public class NotesListActivity extends AppCompatActivity
moveTaskToBack(true);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}

@ -45,7 +45,9 @@ 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.databinding.SettingsHeaderBinding;
// Google Tasks同步功能已禁用
// import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.PasswordActivity;
@ -109,6 +111,11 @@ public class NotesPreferenceActivity extends PreferenceActivity {
*/
private GTaskReceiver mReceiver;
/**
*
*/
private SettingsHeaderBinding mHeaderBinding;
/**
*
*/
@ -141,20 +148,21 @@ public class NotesPreferenceActivity extends PreferenceActivity {
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);
// Google Tasks同步功能已禁用
// mReceiver = new GTaskReceiver();
// IntentFilter filter = new IntentFilter();
// filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
//registerReceiver(mReceiver, filter);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// Android 13 (API 33) 及以上版本需要指定导出标志
registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
// Android 12 及以下版本使用旧方法
registerReceiver(mReceiver, filter);
}
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// // Android 13 (API 33) 及以上版本需要指定导出标志
// registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
// } else {
// // Android 12 及以下版本使用旧方法
// registerReceiver(mReceiver, filter);
// }
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
mHeaderBinding = SettingsHeaderBinding.inflate(getLayoutInflater());
getListView().addHeaderView(mHeaderBinding.getRoot(), null, true);
loadSecurityPreference();
}
@ -202,9 +210,11 @@ public class NotesPreferenceActivity extends PreferenceActivity {
*/
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
// Google Tasks同步功能已禁用
// if (mReceiver != null) {
// unregisterReceiver(mReceiver);
// }
mHeaderBinding = null;
super.onDestroy();
}
@ -283,20 +293,24 @@ public class NotesPreferenceActivity extends PreferenceActivity {
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();
}
// Google Tasks同步功能已禁用
// if (!GTaskSyncService.isSyncing()) {
// if (TextUtils.isEmpty(defaultAccount)) {
// // first time to set account
// showSelectAccountAlertDialog();
// } else {
// // if account has already been set, we need to promp
// // user about risk
// showChangeAccountConfirmAlertDialog();
// }
// } else {
// Toast.makeText(NotesPreferenceActivity.this,
// R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
// .show();
// }
Toast.makeText(NotesPreferenceActivity.this,
"Google Tasks同步功能已禁用", Toast.LENGTH_SHORT)
.show();
return true;
}
});
@ -316,42 +330,50 @@ public class NotesPreferenceActivity extends PreferenceActivity {
* </p>
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
Button syncButton = mHeaderBinding.preferenceSyncButton;
TextView lastSyncTimeView = mHeaderBinding.prefenereceSyncStatusTextview;
// Google Tasks同步功能已禁用
// 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)));
// 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)));
// 禁用同步按钮
syncButton.setEnabled(false);
syncButton.setText("同步功能已禁用");
// 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);
}
}
// 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);
// }
// }
lastSyncTimeView.setText("Google Tasks同步功能已禁用");
lastSyncTimeView.setVisibility(View.VISIBLE);
}
/**
@ -613,11 +635,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
@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));
}
// Google Tasks同步功能已禁用
// 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));
// }
}
}

@ -15,6 +15,7 @@ import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.databinding.ActivityPasswordBinding;
import net.micode.notes.tool.SecurityManager;
import java.util.List;
@ -27,19 +28,16 @@ public class PasswordActivity extends Activity {
private int mMode; // 0: Check, 1: Setup
private int mPasswordType;
private TextView mTvPrompt;
private EditText mEtPin;
private LockPatternView mLockPatternView;
private TextView mTvError;
private Button mBtnCancel;
private ActivityPasswordBinding binding;
private String mFirstInput = null; // For setup confirmation
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_password);
binding = ActivityPasswordBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String action = getIntent().getAction();
if (ACTION_SETUP_PASSWORD.equals(action)) {
@ -56,13 +54,7 @@ public class PasswordActivity extends Activity {
}
private void initViews() {
mTvPrompt = findViewById(R.id.tv_prompt);
mEtPin = findViewById(R.id.et_pin);
mLockPatternView = findViewById(R.id.lock_pattern_view);
mTvError = findViewById(R.id.tv_error);
mBtnCancel = findViewById(R.id.btn_cancel);
mBtnCancel.setOnClickListener(v -> {
binding.btnCancel.setOnClickListener(v -> {
setResult(RESULT_CANCELED);
finish();
});
@ -70,19 +62,19 @@ public class PasswordActivity extends Activity {
private void setupViews() {
if (mMode == 1) { // Setup
mTvPrompt.setText("请设置密码");
binding.tvPrompt.setText("请设置密码");
} else { // Check
mTvPrompt.setText("请输入密码");
binding.tvPrompt.setText("请输入密码");
}
if (mPasswordType == SecurityManager.TYPE_PIN) {
mEtPin.setVisibility(View.VISIBLE);
mLockPatternView.setVisibility(View.GONE);
mEtPin.requestFocus(); // Auto focus
binding.etPin.setVisibility(View.VISIBLE);
binding.lockPatternView.setVisibility(View.GONE);
binding.etPin.requestFocus(); // Auto focus
setupPinLogic();
} else if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mEtPin.setVisibility(View.GONE);
mLockPatternView.setVisibility(View.VISIBLE);
binding.etPin.setVisibility(View.GONE);
binding.lockPatternView.setVisibility(View.VISIBLE);
setupPatternLogic();
} else {
// Should not happen
@ -91,10 +83,10 @@ public class PasswordActivity extends Activity {
}
private void setupPinLogic() {
mEtPin.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE ||
binding.etPin.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
handleInput(mEtPin.getText().toString());
handleInput(binding.etPin.getText().toString());
return true;
}
return false;
@ -102,10 +94,10 @@ public class PasswordActivity extends Activity {
}
private void setupPatternLogic() {
mLockPatternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
binding.lockPatternView.setOnPatternListener(new LockPatternView.OnPatternListener() {
@Override
public void onPatternStart() {
mTvError.setVisibility(View.INVISIBLE);
binding.tvError.setVisibility(View.INVISIBLE);
}
@Override
@ -117,9 +109,9 @@ public class PasswordActivity extends Activity {
@Override
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
if (pattern.size() < 3) {
mTvError.setText("连接至少3个点");
mTvError.setVisibility(View.VISIBLE);
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
binding.tvError.setText("连接至少3个点");
binding.tvError.setVisibility(View.VISIBLE);
binding.lockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
return;
}
handleInput(LockPatternView.patternToString(pattern));
@ -129,30 +121,30 @@ public class PasswordActivity extends Activity {
private void handleInput(String input) {
if (TextUtils.isEmpty(input)) return;
mTvError.setVisibility(View.INVISIBLE);
binding.tvError.setVisibility(View.INVISIBLE);
if (mMode == 0) { // Check
if (SecurityManager.getInstance(this).checkPassword(input)) {
setResult(RESULT_OK);
finish();
} else {
mTvError.setText("密码错误");
mTvError.setVisibility(View.VISIBLE);
binding.tvError.setText("密码错误");
binding.tvError.setVisibility(View.VISIBLE);
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
binding.lockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
} else {
mEtPin.setText("");
binding.etPin.setText("");
}
}
} else { // Setup
if (mFirstInput == null) {
// First entry
mFirstInput = input;
mTvPrompt.setText("请再次输入以确认");
binding.tvPrompt.setText("请再次输入以确认");
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.clearPattern();
binding.lockPatternView.clearPattern();
} else {
mEtPin.setText("");
binding.etPin.setText("");
}
} else {
// Second entry
@ -162,19 +154,25 @@ public class PasswordActivity extends Activity {
setResult(RESULT_OK);
finish();
} else {
mTvError.setText("两次输入不一致,请重试");
mTvError.setVisibility(View.VISIBLE);
binding.tvError.setText("两次输入不一致,请重试");
binding.tvError.setVisibility(View.VISIBLE);
// Reset to start
mFirstInput = null;
mTvPrompt.setText("请设置密码");
binding.tvPrompt.setText("请设置密码");
if (mPasswordType == SecurityManager.TYPE_PATTERN) {
mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
mLockPatternView.postDelayed(() -> mLockPatternView.clearPattern(), 1000);
binding.lockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
binding.lockPatternView.postDelayed(() -> binding.lockPatternView.clearPattern(), 1000);
} else {
mEtPin.setText("");
binding.etPin.setText("");
}
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}

@ -42,6 +42,7 @@ import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.databinding.SidebarLayoutBinding;
import net.micode.notes.viewmodel.FolderListViewModel;
import java.util.ArrayList;
@ -61,14 +62,8 @@ public class SidebarFragment extends Fragment {
private static final String TAG = "SidebarFragment";
private static final int MAX_FOLDER_NAME_LENGTH = 50;
// 视图组件
private RecyclerView rvFolderTree;
private TextView tvRootFolder;
private TextView menuSync;
private TextView menuLogin;
private TextView menuExport;
private TextView menuSettings;
private TextView menuTrash;
// ViewBinding
private SidebarLayoutBinding binding;
// 适配器和数据
private FolderTreeAdapter adapter;
@ -148,17 +143,24 @@ public class SidebarFragment extends Fragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.sidebar_layout, container, false);
binding = SidebarLayoutBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
initViews();
setupListeners();
observeViewModel();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/**
*
*/
@ -171,68 +173,57 @@ public class SidebarFragment extends Fragment {
/**
*
*/
private void initViews(View view) {
rvFolderTree = view.findViewById(R.id.rv_folder_tree);
tvRootFolder = view.findViewById(R.id.tv_root_folder);
menuSync = view.findViewById(R.id.menu_sync);
menuLogin = view.findViewById(R.id.menu_login);
menuExport = view.findViewById(R.id.menu_export);
menuSettings = view.findViewById(R.id.menu_settings);
menuTrash = view.findViewById(R.id.menu_trash);
private void initViews() {
// 设置RecyclerView
rvFolderTree.setLayoutManager(new LinearLayoutManager(requireContext()));
binding.rvFolderTree.setLayoutManager(new LinearLayoutManager(requireContext()));
adapter = new FolderTreeAdapter(new ArrayList<>(), viewModel);
adapter.setOnFolderItemClickListener(this::handleFolderItemClick);
rvFolderTree.setAdapter(adapter);
binding.rvFolderTree.setAdapter(adapter);
}
/**
*
*/
private void setupListeners() {
View view = getView();
if (view == null) return;
// 根文件夹(单击展开/收起,双击跳转)
setupFolderClickListener(tvRootFolder, Notes.ID_ROOT_FOLDER);
setupFolderClickListener(binding.tvRootFolder, Notes.ID_ROOT_FOLDER);
// 关闭侧栏
view.findViewById(R.id.btn_close_sidebar).setOnClickListener(v -> {
binding.btnCloseSidebar.setOnClickListener(v -> {
if (listener != null) {
listener.onCloseSidebar();
}
});
// 创建文件夹
view.findViewById(R.id.btn_create_folder).setOnClickListener(v -> showCreateFolderDialog());
binding.btnCreateFolder.setOnClickListener(v -> showCreateFolderDialog());
// 菜单项
menuSync.setOnClickListener(v -> {
binding.menuSync.setOnClickListener(v -> {
if (listener != null) {
listener.onSyncSelected();
}
});
menuLogin.setOnClickListener(v -> {
binding.menuLogin.setOnClickListener(v -> {
if (listener != null) {
listener.onLoginSelected();
}
});
menuExport.setOnClickListener(v -> {
binding.menuExport.setOnClickListener(v -> {
if (listener != null) {
listener.onExportSelected();
}
});
menuSettings.setOnClickListener(v -> {
binding.menuSettings.setOnClickListener(v -> {
if (listener != null) {
listener.onSettingsSelected();
}
});
menuTrash.setOnClickListener(v -> {
binding.menuTrash.setOnClickListener(v -> {
if (listener != null) {
listener.onTrashSelected();
}

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#88555555" />
<item android:state_selected="true" android:color="#ff999999" />
<item android:color="#ff000000" />
</selector>

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#50000000" />
</selector>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.Material3.Light">
<!-- 搜索框 -->
<androidx.appcompat.widget.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSearch|flagNoExtractUi"
app:iconifiedByDefault="false"
app:queryHint="@string/search_hint" />
</androidx.appcompat.widget.Toolbar>
<!-- 历史记录按钮 -->
<TextView
android:id="@+id/btn_show_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:text="@string/search_history_title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:drawablePadding="8dp"
android:visibility="gone"
app:drawableEndCompat="@android:drawable/arrow_down_float" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:scrollbars="vertical" />
<!-- 无结果提示 -->
<TextView
android:id="@+id/tv_no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:visibility="gone"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -22,30 +22,32 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
android:background="@color/background_color">
<!-- 主内容区域 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 主内容区域 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- AppBarLayout替代传统 ActionBar -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<!-- AppBarLayout替代传统 ActionBar -->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar"
android:fitsSystemWindows="true">
<!-- Toolbar现代替代 ActionBar 的标准组件,带汉堡菜单按钮 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:navigationIcon="@android:drawable/ic_menu_sort_by_size"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<!-- Toolbar现代替代 ActionBar 的标准组件,带汉堡菜单按钮 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/app_name"
app:navigationIcon="@android:drawable/ic_menu_sort_by_size"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</com.google.android.material.appbar.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
<!-- 便签列表:使用 NestedScrollView 包裹 ListView 以支持滚动 -->
<androidx.core.widget.NestedScrollView
@ -59,8 +61,10 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 面包屑导航:显示当前文件夹路径(在列表上方) -->
<include layout="@layout/breadcrumb_layout" />
<!-- 面包屑导航:显示当前文件夹路径(在列表上方) -->
<include
android:id="@+id/breadcrumb_include"
layout="@layout/breadcrumb_layout" />
<!-- 便签列表 -->
<ListView
@ -85,6 +89,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:backgroundTint="@color/fab_color"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_recent_history"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@null" />
<TextView
android:id="@+id/tv_history_keyword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/iv_delete_history"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@string/menu_delete" />
</LinearLayout>

@ -17,7 +17,10 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<color name="primary_color">#1976D2</color>
<color name="primary_color">#263238</color>
<color name="on_primary_color">#FFFFFF</color>
<color name="background_color">#FAFAFA</color>
<color name="background_color">#E8E8E8</color>
<color name="primary_text_dark">#000000</color>
<color name="secondary_text_dark">#808080</color>
<color name="fab_color">#FFC107</color>
</resources>

@ -161,4 +161,9 @@
<string name="delete_confirmation">Are you sure you want to delete selected notes?</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_unlock">Unlock</string>
<!-- Search related -->
<string name="search_no_results">No results found</string>
<string name="search_history_title">Search History</string>
<string name="search_history_clear">Clear</string>
</resources>

@ -4,6 +4,13 @@
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
<!-- 强制背景为白色,防止深色模式下黑屏 -->
<item name="android:colorBackground">@color/background_color</item>
<item name="android:windowBackground">@color/background_color</item>
<!-- 透明导航栏支持edge-to-edge显示 -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- 根据内容自动调整状态栏图标颜色(深色背景=浅色图标) -->
<item name="android:windowLightStatusBar">false</item>
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />

Loading…
Cancel
Save