新增置顶功能 #13

Merged
p7tupf26b merged 1 commits from jiangtianxiang_branch into master 1 month ago

@ -0,0 +1,26 @@
package net.micode.notes;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("net.micode.notes", appContext.getPackageName());
}
}

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Android应用清单文件定义应用的基本信息、权限、组件等 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- ==================== 对于功能的声明 ==================== -->
<!-- 允许写入外部存储,用于保存笔记数据 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 允许在桌面创建快捷方式 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- 允许访问网络,用于同步功能 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 允许读取联系人,可能与笔记关联联系人功能相关 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 允许管理账户,用于账户同步功能 -->
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<!-- 允许验证账户,用于账户同步功能 -->
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<!-- 允许获取设备上的账户列表 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- 允许使用账户凭据 -->
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<!-- 允许接收系统启动完成广播,用于初始化闹钟 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- ==================== 应用配置 ==================== -->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
tools:targetApi="31">
<!-- ==================== 笔记列表活动 ==================== -->
<!-- 应用主入口,显示所有笔记列表 -->
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/Theme.Notesmaster"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<!-- 意图过滤器:定义此活动为应用启动入口 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- ==================== 笔记编辑活动 ==================== -->
<!-- 用于查看、编辑和搜索笔记 -->
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/Theme.Notesmaster.Edit"
android:exported="true">
<!-- 意图过滤器:允许查看笔记 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<!-- 意图过滤器:允许插入或编辑笔记 -->
<intent-filter>
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<!-- 意图过滤器:允许搜索笔记 -->
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- 元数据:指定搜索配置文件 -->
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<!-- ==================== 内容提供者 ==================== -->
<!-- 提供笔记数据的访问接口,允许其他应用访问笔记数据 -->
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true" />
<!-- ==================== 桌面小部件接收器 ==================== -->
<!-- 2x2大小的笔记桌面小部件 -->
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2"
android:exported="true">
<intent-filter>
<!-- 小部件更新事件 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!-- 小部件删除事件 -->
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<!-- 隐私模式变更事件 -->
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<!-- 小部件配置信息 -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_2x_info" />
</receiver>
<!-- 4x4大小的笔记桌面小部件 -->
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4"
android:exported="true">
<intent-filter>
<!-- 小部件更新事件 -->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<!-- 小部件删除事件 -->
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<!-- 隐私模式变更事件 -->
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<!-- 小部件配置信息 -->
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_4x_info" />
</receiver>
<!-- ==================== 闹钟初始化接收器 ==================== -->
<!-- 系统启动完成后初始化闹钟 -->
<receiver android:name=".ui.AlarmInitReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<!-- ==================== 闹钟接收器 ==================== -->
<!-- 接收闹钟触发事件,运行在独立进程中 -->
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<!-- ==================== 闹钟提醒活动 ==================== -->
<!-- 显示闹钟提醒界面 -->
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<!-- ==================== 设置活动 ==================== -->
<!-- 应用设置界面 -->
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<!-- ==================== 同步服务 ==================== -->
<!-- Google任务同步服务用于与Google Tasks同步数据 -->
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<!-- ==================== 搜索元数据 ==================== -->
<!-- 指定默认的搜索活动为NoteEditActivity -->
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />
<!-- 注释掉的MainActivity当前未使用 -->
<!-- <activity-->
<!-- android:name=".MainActivity"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
</application>
</manifest>

@ -0,0 +1,159 @@
package net.micode.notes;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import net.micode.notes.data.Notes;
import net.micode.notes.ui.SidebarFragment;
/**
*
* <p>
*
*
* </p>
*/
public class MainActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener {
private static final String TAG = "MainActivity";
private DrawerLayout drawerLayout;
/**
*
* <p>
*
* </p>
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用边到边显示模式
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
// 初始化DrawerLayout
drawerLayout = findViewById(R.id.drawer_layout);
if (drawerLayout != null) {
// 设置侧栏在左侧
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, Gravity.LEFT);
// 设置监听器:侧栏关闭时更新状态
drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
// 侧栏滑动时
}
@Override
public void onDrawerOpened(View drawerView) {
// 侧栏打开时
}
@Override
public void onDrawerClosed(View drawerView) {
// 侧栏关闭时
}
@Override
public void onDrawerStateChanged(int newState) {
// 侧栏状态改变时
}
});
}
// 设置窗口边距监听器,自动适配系统栏
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_content), (v, insets) -> {
// 获取系统栏边距
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
// 设置视图内边距以适配系统栏
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
// 启动NotesListActivity作为主界面
Intent intent = new Intent(this, net.micode.notes.ui.NotesListActivity.class);
startActivity(intent);
}
// ==================== SidebarFragment.OnSidebarItemSelectedListener 实现 ====================
@Override
public void onFolderSelected(long folderId) {
Log.d(TAG, "Folder selected: " + folderId);
// 打开侧栏中的文件夹:不关闭侧栏,直接切换视图
// 这个回调通常用于侧栏中的文件夹项双击
// 实际跳转逻辑应该在NotesListActivity中处理
closeSidebar();
}
@Override
public void onTrashSelected() {
Log.d(TAG, "Trash selected");
// TODO: 实现跳转到回收站
// 关闭侧栏
closeSidebar();
}
@Override
public void onSyncSelected() {
Log.d(TAG, "Sync selected");
// TODO: 实现同步功能
}
@Override
public void onLoginSelected() {
Log.d(TAG, "Login selected");
// TODO: 实现登录功能
}
@Override
public void onExportSelected() {
Log.d(TAG, "Export selected");
// TODO: 实现导出功能
}
@Override
public void onSettingsSelected() {
Log.d(TAG, "Settings selected");
// 打开设置界面
Intent intent = new Intent(this, net.micode.notes.ui.NotesPreferenceActivity.class);
startActivity(intent);
// 关闭侧栏
closeSidebar();
}
@Override
public void onCreateFolder() {
Log.d(TAG, "Create folder");
// 创建文件夹功能由SidebarFragment内部处理
// 这里不需要做任何事情
}
@Override
public void onCloseSidebar() {
closeSidebar();
}
// ==================== 私有方法 ====================
/**
*
*/
private void closeSidebar() {
if (drawerLayout != null) {
drawerLayout.closeDrawer(Gravity.LEFT);
}
}
}

@ -0,0 +1,131 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import java.util.HashMap;
/**
*
* <p>
* 使
* AndroidContactsContract Provider
* </p>
* <p>
*
* <ul>
* <li></li>
* <li>使HashMap</li>
* <li></li>
* </ul>
* </p>
* <p>
* 使
* 使
* </p>
*
* @see ContactsContract
* @see PhoneNumberUtils
*/
public class Contact {
/**
*
* <p>
* 使HashMap
*
* </p>
* Key:
* Value:
*/
private static HashMap<String, String> sContactCache;
/**
*
*/
private static final String TAG = "Contact";
/**
* SQL
* <p>
* 使PHONE_NUMBERS_EQUAL
* Phone.CONTENT_ITEM_TYPE
* 使min_match='+'
* </p>
*/
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
/**
*
* <p>
*
*
*
* </p>
*
* @param context 访ContentResolver
* @param phoneNumber
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
// 初始化缓存
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
// 检查缓存中是否已存在
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
// 构建查询条件使用toCallerIDMinMatch进行号码最小匹配
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
// 处理查询结果
if (cursor != null && cursor.moveToFirst()) {
try {
String name = cursor.getString(0);
sContactCache.put(phoneNumber, name);
return name;
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
cursor.close();
}
} else {
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

@ -0,0 +1,355 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.net.Uri;
/**
*
* <p>
* 使
* <ul>
* <li>Content ProviderAuthorityURI</li>
* <li></li>
* <li>ID</li>
* <li>Intent Extra</li>
* <li>Widget</li>
* <li>NoteColumnsDataColumns</li>
* <li></li>
* </ul>
* </p>
* <p>
* Content Provider
* 访便使
* </p>
*/
public class Notes {
/**
* Content ProviderAuthority
*/
public static final String AUTHORITY = "micode_notes";
/**
*
*/
public static final String TAG = "Notes";
/**
*
*/
public static final int TYPE_NOTE = 0;
/**
*
*/
public static final int TYPE_FOLDER = 1;
/**
*
*/
public static final int TYPE_SYSTEM = 2;
/**
* ID
* {@link Notes#ID_ROOT_FOLDER }
* {@link Notes#ID_TEMPARAY_FOLDER }
* {@link Notes#ID_CALL_RECORD_FOLDER}
*/
public static final int ID_ROOT_FOLDER = 0;
/**
* ID
*/
public static final int ID_TEMPARAY_FOLDER = -1;
/**
* ID
*/
public static final int ID_CALL_RECORD_FOLDER = -2;
/**
* ID
*/
public static final int ID_TRASH_FOLER = -3;
/**
* Intent Extra
*/
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
/**
* Intent ExtraID
*/
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
/**
* Intent ExtraWidget ID
*/
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
/**
* Intent ExtraWidget
*/
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
/**
* Intent ExtraID
*/
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
/**
* Intent Extra
*/
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
/**
* Widget
*/
public static final int TYPE_WIDGET_INVALIDE = -1;
/**
* 2x2 Widget
*/
public static final int TYPE_WIDGET_2X = 0;
/**
* 4x4 Widget
*/
public static final int TYPE_WIDGET_4X = 1;
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
* Uri to query all notes and folders
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri to query data
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
public interface NoteColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The parent's id for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Alert date
* <P> Type: INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* Folder's name or text content of note
* <P> Type: TEXT </P>
*/
public static final String SNIPPET = "snippet";
/**
* Note's widget id
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
* Note's widget type
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* Note's background color's id
* <P> Type: INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
* For text note, it doesn't has attachment, for multi-media
* note, it has at least one attachment
* <P> Type: INTEGER </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
* Folder's count of notes
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
* The file type: folder or note
* <P> Type: INTEGER </P>
*/
public static final String TYPE = "type";
/**
* The last sync id
* <P> Type: INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
* Sign to indicate local modified or not
* <P> Type: INTEGER </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* Original parent id before moving into temporary folder
* <P> Type : INTEGER </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* The gtask id
* <P> Type : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
* The version code
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
/**
* Sign to indicate the note is pinned to top or not
* <P> Type : INTEGER </P>
*/
public static final String TOP = "top";
}
public interface DataColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The MIME type of the item represented by this row.
* <P> Type: Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* The reference id to note that this data belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Data's content
* <P> Type: TEXT </P>
*/
public static final String CONTENT = "content";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA5 = "data5";
}
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
* <P> Type: Integer 1:check list mode 0: normal mode </P>
*/
public static final String MODE = DATA1;
public static final int MODE_CHECK_LIST = 1;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
public static final class CallNote implements DataColumns {
/**
* Call date for this record
* <P> Type: INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;
/**
* Phone number for this record
* <P> Type: TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}

@ -0,0 +1,611 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* <p>
* SQLiteOpenHelperSQLite
* notedata
* 使
* </p>
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li>4</li>
* <li>访</li>
* </ul>
* </p>
* <p>
*
* <ul>
* <li>V1: </li>
* <li>V2: </li>
* <li>V3: GTASK_ID</li>
* <li>V4: VERSION</li>
* </ul>
* </p>
*
* @see SQLiteOpenHelper
* @see Notes
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
*
*/
private static final String DB_NAME = "note.db";
/**
*
* <p>
* 5
* onUpgrade
* </p>
*/
private static final int DB_VERSION = 5;
/**
*
*/
public interface TABLE {
/**
*
* <p>
* IDID
* Widget
* IDIDGTASK ID
* </p>
*/
public static final String NOTE = "note";
/**
*
* <p>
* MIME
* MIME5
* </p>
*/
public static final String DATA = "data";
}
/**
*
*/
private static final String TAG = "NotesDatabaseHelper";
/**
*
* <p>
* 使
*
* </p>
*/
private static NotesDatabaseHelper mInstance;
/**
* SQL
* <p>
* note
* <ul>
* <li>ID: </li>
* <li>PARENT_ID: ID0</li>
* <li>ALERTED_DATE: 0</li>
* <li>BG_COLOR_ID: ID0</li>
* <li>CREATED_DATE: </li>
* <li>HAS_ATTACHMENT: 0</li>
* <li>MODIFIED_DATE: </li>
* <li>NOTES_COUNT: 0</li>
* <li>SNIPPET: </li>
* <li>TYPE: 0=1=2=0</li>
* <li>WIDGET_ID: Widget ID0</li>
* <li>WIDGET_TYPE: Widget-1</li>
* <li>SYNC_ID: ID0</li>
* <li>LOCAL_MODIFIED: 0</li>
* <li>ORIGIN_PARENT_ID: ID0</li>
* <li>GTASK_ID: Google Tasks ID</li>
* <li>VERSION: 0</li>
* </ul>
* </p>
*/
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
/**
* SQL
* <p>
* data
* <ul>
* <li>ID: </li>
* <li>MIME_TYPE: MIME</li>
* <li>NOTE_ID: ID0</li>
* <li>CREATED_DATE: </li>
* <li>MODIFIED_DATE: </li>
* <li>CONTENT: </li>
* <li>DATA1-5: </li>
* </ul>
* </p>
*/
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
/**
* SQL
* <p>
* dataNOTE_IDID
* </p>
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* Increase folder's note count when move note to the folder
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when move note from folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
* Increase folder's note count when insert new note to the folder
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when delete note from the folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Delete datas belong to note which has been deleted
*/
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Delete notes belong to folder which has been deleted
*/
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Move notes belong to folder which has been moved to trash folder
*/
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
*
*
* @param context
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
*
* <p>
* noteSQL
* </p>
*
* @param db SQLiteDatabase
*/
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
/**
*
* <p>
* note
*
* </p>
*
* @param db SQLiteDatabase
*/
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
// 删除所有已存在的触发器
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
// 重新创建所有触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/**
*
* <p>
* note
* <ul>
* <li>ID_CALL_RECORD_FOLDER</li>
* <li>ID_ROOT_FOLDER</li>
* <li>ID_TEMPARAY_FOLDER</li>
* <li>ID_TRASH_FOLER</li>
* </ul>
* </p>
*
* @param db SQLiteDatabase
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* call record foler for call notes
*/
// 创建通话记录文件夹
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* root folder which is default folder
*/
// 创建根文件夹(默认文件夹)
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* temporary folder which is used for moving note
*/
// 创建临时文件夹(用于移动笔记)
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create trash folder
*/
// 创建回收站文件夹
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
*
* <p>
* dataSQL
* </p>
*
* @param db SQLiteDatabase
*/
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
/**
*
* <p>
* data
*
* </p>
*
* @param db SQLiteDatabase
*/
private void reCreateDataTableTriggers(SQLiteDatabase db) {
// 删除所有已存在的触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 重新创建所有触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
/**
*
* <p>
* 使线
* </p>
*
* @param context
* @return NotesDatabaseHelper
*/
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
/**
*
* <p>
* notedata
* </p>
*
* @param db SQLiteDatabase
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
/**
*
* <p>
*
*
* </p>
*
* @param db SQLiteDatabase
* @param oldVersion
* @param newVersion
* @throws IllegalStateException
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
// 从V1升级到V2包括V2到V3
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
// 从V2升级到V3
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
// 从V3升级到V4
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 如果需要,重新创建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 检查升级是否成功
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
/**
* V2
* <p>
* notedata
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
createNoteTable(db);
createDataTable(db);
}
/**
* V3
* <p>
* GTASK_IDnote
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
// 删除未使用的触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
// 添加GTASK_ID列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
// 添加回收站系统文件夹
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
* V4
* <p>
* VERSIONnote
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
/**
* V5
* <p>
* TOPnote
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TOP
+ " INTEGER NOT NULL DEFAULT 0");
}
}

@ -0,0 +1,517 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* Content Provider
* <p>
* ContentProviderCRUD
* notedata访URI
*
* </p>
* <p>
*
* <ul>
* <li>URIURI</li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
* <p>
* URI
* <ul>
* <li>content://micode_notes/note - 查询所有笔记</li>
* <li>content://micode_notes/note/# - 查询指定ID的笔记</li>
* <li>content://micode_notes/data - 查询所有数据</li>
* <li>content://micode_notes/data/# - 查询指定ID的数据</li>
* <li>content://micode_notes/search - 搜索笔记</li>
* <li>content://micode_notes/search_suggest_query - 搜索建议</li>
* </ul>
* </p>
*
* @see ContentProvider
* @see NotesDatabaseHelper
* @see Notes
*/
public class NotesProvider extends ContentProvider {
/**
* URI
* <p>
* URI
* URI
* </p>
*/
private static final UriMatcher mMatcher;
/**
*
* <p>
* SQLiteDatabase
* </p>
*/
private NotesDatabaseHelper mHelper;
/**
*
*/
private static final String TAG = "NotesProvider";
/**
* URI
*/
private static final int URI_NOTE = 1;
/**
* URI
*/
private static final int URI_NOTE_ITEM = 2;
/**
* URI
*/
private static final int URI_DATA = 3;
/**
* URI
*/
private static final int URI_DATA_ITEM = 4;
/**
* URI
*/
private static final int URI_SEARCH = 5;
/**
* URI
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* URI
* <p>
* UriMatcherURI
* </p>
*/
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
/**
*
* <p>
* IDIntent
* 使TRIMREPLACE便
* </p>
* <p>
* x'0A'SQLite'\n'
*
* </p>
*/
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
/**
* SQL
* <p>
* noteSNIPPET
* PARENT_IDID_TRASH_FOLER
* TYPETYPE_NOTE
* </p>
*/
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
/**
* Content Provider
* <p>
*
* </p>
*
* @return true
*/
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
/**
*
* <p>
* URI
* 使LIKE
* </p>
*
* @param uri URI
* @param projection
* @param selection
* @param selectionArgs
* @param sortOrder
* @return Cursor
* @throws IllegalArgumentException URI
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 查询所有笔记
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
// 查询指定ID的笔记
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
// 查询所有数据
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
// 查询指定ID的数据
id = uri.getPathSegments().get(1);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 搜索笔记或搜索建议
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
// 从URI路径中获取搜索关键词
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
// 从查询参数中获取搜索关键词
searchString = uri.getQueryParameter("pattern");
}
// 搜索关键词为空时返回null
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
// 使用模糊匹配搜索笔记摘要
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 设置通知URI当数据变更时通知观察者
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
/**
*
* <p>
* URI
* URI
* </p>
*
* @param uri URI
* @param values
* @return URIID
* @throws IllegalArgumentException URI
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 插入笔记
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
// 插入数据
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Notify the note uri
// 通知笔记URI的观察者
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// Notify the data uri
// 通知数据URI的观察者
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
return ContentUris.withAppendedId(uri, insertedId);
}
/**
*
* <p>
* URI
* ID0
* URI
* </p>
*
* @param uri URI
* @param selection
* @param selectionArgs
* @return
* @throws IllegalArgumentException URI
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 删除笔记(排除系统文件夹)
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 删除指定ID的笔记
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
* ID0
*/
long noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
}
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 删除数据
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true;
break;
case URI_DATA_ITEM:
// 删除指定ID的数据
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 删除成功后通知观察者
if (count > 0) {
if (deleteData) {
// 删除数据时通知笔记URI
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
*
* <p>
* URI
*
* URI
* </p>
*
* @param uri URI
* @param values
* @param selection
* @param selectionArgs
* @return
* @throws IllegalArgumentException URI
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 更新笔记(递增版本号)
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
// 更新指定ID的笔记递增版本号
id = uri.getPathSegments().get(1);
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA:
// 更新数据
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
case URI_DATA_ITEM:
// 更新指定ID的数据
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 更新成功后通知观察者
if (count > 0) {
if (updateData) {
// 更新数据时通知笔记URI
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
/**
*
* <p>
* IDSQL WHERE
* </p>
*
* @param selection
* @return
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
/**
*
* <p>
* VERSION使1
*
* </p>
*
* @param id ID0使selection
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
// 构建WHERE子句
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
if (id > 0) {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
if (!TextUtils.isEmpty(selection)) {
String selectString = id > 0 ? parseSelection(selection) : selection;
// 替换查询条件中的占位符
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
sql.append(selectString);
}
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* MIME
* <p>
* URIMIME
* </p>
*
* @param uri URI
* @return MIME
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}

@ -0,0 +1,890 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.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.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.Note;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
/**
*
* <p>
* 访Content Provider
*
* </p>
* <p>
* 使Executor线访UI线
* </p>
*
* @see Note
* @see Notes
*/
public class NotesRepository {
/**
*
* <p>
*
* </p>
*/
public static class NoteInfo {
public long id;
public String title;
public String snippet;
public long parentId;
public long createdDate;
public long modifiedDate;
public int type;
public int localModified;
public int bgColorId;
public boolean isPinned; // 新增置顶字段
public NoteInfo() {}
public long getId() {
return id;
}
public long getParentId() {
return parentId;
}
public String getNoteDataValue() {
return snippet;
}
}
private static final String TAG = "NotesRepository";
private final ContentResolver contentResolver;
private final ExecutorService executor;
// 选择条件常量
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + " = ?";
private static final String ROOT_FOLDER_SELECTION = "(" +
NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " +
NoteColumns.PARENT_ID + "=?) OR (" +
NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " +
NoteColumns.NOTES_COUNT + ">0)";
/**
* 访
* <p>
* 访
* </p>
*
* @param <T>
*/
public interface Callback<T> {
/**
*
*
* @param result
*/
void onSuccess(T result);
/**
*
*
* @param error
*/
void onError(Exception error);
}
/**
* Cursor NoteInfo
*
* @param cursor
* @return NoteInfo
*/
private NoteInfo noteFromCursor(Cursor cursor) {
NoteInfo noteInfo = new NoteInfo();
noteInfo.id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
noteInfo.title = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
noteInfo.snippet = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
noteInfo.parentId = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.PARENT_ID));
noteInfo.createdDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.CREATED_DATE));
noteInfo.modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
noteInfo.type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
noteInfo.localModified = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCAL_MODIFIED));
int bgColorIdIndex = cursor.getColumnIndex(NoteColumns.BG_COLOR_ID);
if (bgColorIdIndex != -1 && !cursor.isNull(bgColorIdIndex)) {
noteInfo.bgColorId = cursor.getInt(bgColorIdIndex);
} else {
noteInfo.bgColorId = 0;
}
int topIndex = cursor.getColumnIndex(NoteColumns.TOP);
if (topIndex != -1) {
noteInfo.isPinned = cursor.getInt(topIndex) > 0;
}
return noteInfo;
}
/**
*
* <p>
* ContentResolver线
* </p>
*
* @param contentResolver Content
*/
public NotesRepository(ContentResolver contentResolver) {
this.contentResolver = contentResolver;
// 使用单线程Executor确保数据访问的顺序性
this.executor = java.util.concurrent.Executors.newSingleThreadExecutor();
Log.d(TAG, "NotesRepository initialized");
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID{@link Notes#ID_ROOT_FOLDER}
* @param callback
*/
public void getNotes(long folderId, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
List<NoteInfo> notes = queryNotes(folderId);
callback.onSuccess(notes);
Log.d(TAG, "Successfully loaded notes for folder: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to load notes for folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
* 便便
* </p>
*
* @param folderId ID
* @return 便
*/
private List<NoteInfo> queryNotes(long folderId) {
List<NoteInfo> notes = new ArrayList<>();
List<NoteInfo> folders = new ArrayList<>();
List<NoteInfo> normalNotes = new ArrayList<>();
String selection;
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
// 根文件夹:显示所有文件夹和便签
selection = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?) OR (" +
NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)";
selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)};
} else {
// 子文件夹:显示该文件夹下的文件夹和便签
selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM;
selectionArgs = new String[]{String.valueOf(folderId)};
}
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
NoteInfo note = noteFromCursor(cursor);
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹单独收集
folders.add(note);
} else if (note.type == Notes.TYPE_NOTE) {
// 便签收集
normalNotes.add(note);
} else if (note.type == Notes.TYPE_SYSTEM && note.id == Notes.ID_CALL_RECORD_FOLDER) {
// 通话记录文件夹
folders.add(note);
}
}
Log.d(TAG, "Query returned " + folders.size() + " folders and " + normalNotes.size() + " notes");
} finally {
cursor.close();
}
}
// 文件夹按修改时间倒序排列
folders.sort((a, b) -> Long.compare(b.modifiedDate, a.modifiedDate));
// 便签按修改时间倒序排列
normalNotes.sort((a, b) -> {
// 首先按置顶状态排序(置顶在前)
if (a.isPinned != b.isPinned) {
return a.isPinned ? -1 : 1;
}
// 其次按修改时间倒序排列
return Long.compare(b.modifiedDate, a.modifiedDate);
});
// 合并:文件夹在前,便签在后
notes.addAll(folders);
notes.addAll(normalNotes);
return notes;
}
/**
*
*
* @param folderId ID
* @return null
*/
public NoteInfo getFolderInfo(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER) {
NoteInfo root = new NoteInfo();
root.id = Notes.ID_ROOT_FOLDER;
root.title = "我的便签";
root.snippet = "我的便签";
root.type = Notes.TYPE_FOLDER;
return root;
}
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = new String[]{String.valueOf(folderId)};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
null
);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return noteFromCursor(cursor);
}
} finally {
cursor.close();
}
}
return null;
}
/**
* ID
*
* @param folderId ID
* @param callback ID
*/
public void getParentFolderId(long folderId, Callback<Long> callback) {
executor.execute(() -> {
try {
long parentId = getParentFolderId(folderId);
callback.onSuccess(parentId);
} catch (Exception e) {
callback.onError(e);
}
});
}
/**
* ID
*
* @param folderId ID
* @return IDID
*/
public long getParentFolderId(long folderId) {
if (folderId == Notes.ID_ROOT_FOLDER || folderId == Notes.ID_CALL_RECORD_FOLDER) {
return Notes.ID_ROOT_FOLDER;
}
NoteInfo folder = getFolderInfo(folderId);
if (folder != null) {
return folder.parentId;
}
return Notes.ID_ROOT_FOLDER;
}
/**
*
*
* @param folderId ID
* @return
*/
public List<NoteInfo> getFolderPath(long folderId) {
List<NoteInfo> path = new ArrayList<>();
long currentId = folderId;
while (currentId != Notes.ID_ROOT_FOLDER) {
NoteInfo folder = getFolderInfo(currentId);
if (folder == null) {
break;
}
path.add(0, folder); // 添加到列表头部
currentId = folder.parentId;
}
// 添加根文件夹
NoteInfo root = new NoteInfo();
root.id = Notes.ID_ROOT_FOLDER;
root.title = "我的便签";
root.snippet = "我的便签";
root.type = Notes.TYPE_FOLDER;
path.add(0, root);
return path;
}
/**
*
*
* @param folderId ID
* @param callback
*/
public void getFolderPath(long folderId, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
List<NoteInfo> path = getFolderPath(folderId);
callback.onSuccess(path);
} catch (Exception e) {
callback.onError(e);
}
});
}
/**
*
*
* @param parentId ID
* @param name
* @param callback ID
*/
public void createFolder(long parentId, String name, Callback<Long> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, parentId);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.NOTES_COUNT, 0);
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long folderId = 0L;
if (uri != null) {
try {
folderId = ContentUris.parseId(uri);
} catch (Exception e) {
Log.e(TAG, "Failed to parse folder ID from URI", e);
}
}
callback.onSuccess(folderId);
Log.d(TAG, "Successfully created folder: " + name + " with ID: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to create folder: " + name, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback ID
*/
public void createNote(long folderId, Callback<Long> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, folderId);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.SNIPPET, "");
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long noteId = 0L;
if (uri != null) {
try {
noteId = ContentUris.parseId(uri);
} catch (Exception e) {
Log.e(TAG, "Failed to parse note ID from URI", e);
}
}
if (noteId > 0) {
callback.onSuccess(noteId);
Log.d(TAG, "Successfully created note with ID: " + noteId);
} else {
callback.onError(new IllegalStateException("Failed to create note, invalid ID returned"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to create note", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
* @param content
* @param callback
*/
public void updateNote(long noteId, String content, Callback<Integer> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.SNIPPET, extractSnippet(content));
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
if (rows > 0) {
// 查询现有的文本数据记录
Cursor cursor = contentResolver.query(
Notes.CONTENT_DATA_URI,
new String[]{DataColumns.ID},
DataColumns.NOTE_ID + " = ? AND " + DataColumns.MIME_TYPE + " = ?",
new String[]{String.valueOf(noteId), TextNote.CONTENT_ITEM_TYPE},
null
);
long dataId = 0;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
dataId = cursor.getLong(cursor.getColumnIndexOrThrow(DataColumns.ID));
}
} finally {
cursor.close();
}
}
// 更新或插入文本数据
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.CONTENT, content);
if (dataId > 0) {
// 更新现有记录
Uri dataUri = ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId);
int dataRows = contentResolver.update(dataUri, dataValues, null, null);
if (dataRows > 0) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully updated note: " + noteId);
} else {
callback.onError(new RuntimeException("Failed to update note data"));
}
} else {
// 插入新记录
dataValues.put(DataColumns.NOTE_ID, noteId);
dataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri dataUri = contentResolver.insert(Notes.CONTENT_DATA_URI, dataValues);
if (dataUri != null) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully updated note: " + noteId);
} else {
callback.onError(new RuntimeException("Failed to insert note data"));
}
}
} else {
callback.onError(new RuntimeException("No note found with ID: " + noteId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to update note: " + noteId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
* @param callback
*/
public void deleteNote(long noteId, Callback<Integer> callback) {
executor.execute(() -> {
try {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
if (rows > 0) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully moved note to trash: " + noteId);
} else {
callback.onError(new RuntimeException("No note found with ID: " + noteId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to delete note: " + noteId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteIds ID
* @param callback
*/
public void deleteNotes(List<Long> noteIds, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
for (Long noteId : noteIds) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully moved " + totalRows + " notes to trash");
} else {
callback.onError(new RuntimeException("No notes were deleted"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to batch delete notes", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param keyword
* @param callback
*/
public void searchNotes(String keyword, Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
if (keyword == null || keyword.trim().isEmpty()) {
callback.onSuccess(new ArrayList<>());
return;
}
String selection = "(" + NoteColumns.TYPE + " = ?) AND (" +
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),
"%" + keyword + "%",
"%" + keyword + "%"
};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
List<NoteInfo> notes = new ArrayList<>();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
notes.add(noteFromCursor(cursor));
}
Log.d(TAG, "Search returned " + cursor.getCount() + " results for: " + keyword);
} finally {
cursor.close();
}
}
callback.onSuccess(notes);
} catch (Exception e) {
Log.e(TAG, "Failed to search notes: " + keyword, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback
*/
public void countNotes(long folderId, Callback<Integer> callback) {
executor.execute(() -> {
try {
String selection;
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
selection = NoteColumns.TYPE + " != ?";
selectionArgs = new String[]{String.valueOf(Notes.TYPE_FOLDER)};
} else {
selection = NoteColumns.PARENT_ID + " = ?";
selectionArgs = new String[]{String.valueOf(folderId)};
}
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
new String[]{"COUNT(*) AS count"},
selection,
selectionArgs,
null
);
int count = 0;
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
}
} finally {
cursor.close();
}
}
callback.onSuccess(count);
Log.d(TAG, "Counted " + count + " notes in folder: " + folderId);
} catch (Exception e) {
Log.e(TAG, "Failed to count notes in folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param callback
*/
public void getFolders(Callback<List<NoteInfo>> callback) {
executor.execute(() -> {
try {
String selection = NoteColumns.TYPE + " = ?";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_FOLDER)
};
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
null,
selection,
selectionArgs,
NoteColumns.MODIFIED_DATE + " DESC"
);
List<NoteInfo> folders = new ArrayList<>();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
folders.add(noteFromCursor(cursor));
}
Log.d(TAG, "Found " + cursor.getCount() + " folders");
} finally {
cursor.close();
}
}
callback.onSuccess(folders);
} catch (Exception e) {
Log.e(TAG, "Failed to load folders", e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteIds ID
* @param targetFolderId ID
* @param callback
*/
public void moveNotes(List<Long> noteIds, long targetFolderId, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
for (Long noteId : noteIds) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, targetFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully moved " + totalRows + " notes to folder: " + targetFolderId);
} else {
callback.onError(new RuntimeException("No notes were moved"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to move notes", e);
callback.onError(e);
}
});
}
/**
*
*
* @param noteIds ID
* @param isPinned
* @param callback
*/
public void batchTogglePin(List<Long> noteIds, boolean isPinned, Callback<Integer> callback) {
executor.execute(() -> {
try {
if (noteIds == null || noteIds.isEmpty()) {
callback.onError(new IllegalArgumentException("Note IDs list is empty"));
return;
}
int totalRows = 0;
ContentValues values = new ContentValues();
values.put(NoteColumns.TOP, isPinned ? 1 : 0);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
for (Long noteId : noteIds) {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
totalRows += rows;
}
if (totalRows > 0) {
callback.onSuccess(totalRows);
Log.d(TAG, "Successfully updated pin state for " + totalRows + " notes");
} else {
callback.onError(new RuntimeException("No notes were updated"));
}
} catch (Exception e) {
Log.e(TAG, "Failed to update pin state", e);
callback.onError(e);
}
});
}
/**
*
*
* @param content
* @return 100
*/
private String extractSnippet(String content) {
if (content == null || content.isEmpty()) {
return "";
}
int maxLength = 100;
return content.length() > maxLength
? content.substring(0, maxLength)
: content;
}
/**
* Executor
* <p>
* 访线
* </p>
*/
public void shutdown() {
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
Log.d(TAG, "Executor shutdown");
}
}
}

@ -0,0 +1,160 @@
/*
* 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");
}
}

@ -0,0 +1,245 @@
/*
* 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;
}
}

@ -0,0 +1,269 @@
/*
* 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;
}
}

@ -0,0 +1,668 @@
/*
* 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;
}
}

@ -0,0 +1,499 @@
/*
* 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;
}
}

@ -0,0 +1,510 @@
/*
* 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;
}
}

@ -0,0 +1,55 @@
/*
* 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);
}
}

@ -0,0 +1,55 @@
/*
* 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);
}
}

@ -0,0 +1,222 @@
/*
* 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();
}
}
}

@ -0,0 +1,784 @@
/*
* 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;
}
}

@ -0,0 +1,857 @@
/*
* 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;
}
}

@ -0,0 +1,241 @@
/*
* 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,425 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import java.util.ArrayList;
/**
*
* <p>
*
*
* </p>
*/
public class Note {
/** 笔记差异值,用于记录需要同步的字段变更 */
private ContentValues mNoteDiffValues;
/** 笔记数据对象,包含文本数据和通话数据 */
private NoteData mNoteData;
/** 日志标签 */
private static final String TAG = "Note";
/**
* ID
* <p>
* ID
* ID
* </p>
*
* @param context
* @param folderId ID
* @return ID 0
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// 在数据库中创建新笔记
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
values.put(NoteColumns.MODIFIED_DATE, createdTime);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.PARENT_ID, folderId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
try {
// 从 URI 中提取笔记 ID
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
/**
*
* <p>
*
* </p>
*/
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
/**
*
* <p>
*
* </p>
*
* @param key
* @param value
*/
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* <p>
*
* </p>
*
* @param key
* @param value
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
/**
* ID
*
* @return ID
*/
public long getTextDataId() {
return mNoteData.mTextDataId;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
/**
*
* <p>
*
* </p>
*
* @param key
* @param value
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
/**
*
* <p>
*
* </p>
*
* @return true false
*/
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
/**
*
* <p>
*
*
* </p>
*
* @param context
* @param noteId ID
* @return true false
*/
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
if (!isLocalModified()) {
return true;
}
/**
* {@link NoteColumns#LOCAL_MODIFIED}
* {@link NoteColumns#MODIFIED_DATE}使
*/
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// 不返回,继续执行
}
mNoteDiffValues.clear();
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
return false;
}
return true;
}
/**
*
* <p>
*
*
* </p>
*/
private class NoteData {
/** 文本数据 ID */
private long mTextDataId;
/** 文本数据值 */
private ContentValues mTextDataValues;
/** 通话数据 ID */
private long mCallDataId;
/** 通话数据值 */
private ContentValues mCallDataValues;
/** 日志标签 */
private static final String TAG = "NoteData";
/**
*
* <p>
* ContentValues
* </p>
*/
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
/**
*
* <p>
*
* </p>
*
* @return true false
*/
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID 0
*/
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID 0
*/
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
/**
*
* <p>
*
* </p>
*
* @param key
* @param value
*/
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* <p>
*
* </p>
*
* @param key
* @param value
*/
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
* ContentResolver
* <p>
*
*
* </p>
*
* @param context
* @param noteId ID
* @return URI null
*/
Uri pushIntoContentResolver(Context context, long noteId) {
/**
*
*/
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null;
// 处理文本数据
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
// 新增文本数据
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
// 更新现有文本数据
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear();
}
// 处理通话数据
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
// 新增通话数据
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
// 更新现有通话数据
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear();
}
// 批量执行更新操作
if (operationList.size() > 0) {
try {
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
return null;
}
}
}

@ -0,0 +1,616 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
/**
*
* <p>
*
*
* </p>
*/
public class WorkingNote {
/** 底层笔记对象 */
private Note mNote;
/** 笔记 ID */
private long mNoteId;
/** 笔记内容 */
private String mContent;
/** 笔记模式 */
private int mMode;
/** 提醒日期 */
private long mAlertDate;
/** 修改日期 */
private long mModifiedDate;
/** 背景颜色 ID */
private int mBgColorId;
/** Widget ID */
private int mWidgetId;
/** Widget 类型 */
private int mWidgetType;
/** 父文件夹 ID */
private long mFolderId;
/** 应用上下文 */
private Context mContext;
/** 日志标签 */
private static final String TAG = "WorkingNote";
/** 是否已删除 */
private boolean mIsDeleted;
/** 笔记设置变更监听器 */
private NoteSettingChangedListener mNoteSettingStatusListener;
/** 数据查询投影 - 笔记数据 */
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
/** 数据查询投影 - 笔记元数据 */
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE
};
/** 数据 ID 列索引 */
private static final int DATA_ID_COLUMN = 0;
/** 数据内容列索引 */
private static final int DATA_CONTENT_COLUMN = 1;
/** 数据 MIME 类型列索引 */
private static final int DATA_MIME_TYPE_COLUMN = 2;
/** 数据模式列索引 */
private static final int DATA_MODE_COLUMN = 3;
/** 笔记父 ID 列索引 */
private static final int NOTE_PARENT_ID_COLUMN = 0;
/** 笔记提醒日期列索引 */
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
/** 笔记背景颜色 ID 列索引 */
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
/** 笔记 Widget ID 列索引 */
private static final int NOTE_WIDGET_ID_COLUMN = 3;
/** 笔记 Widget 类型列索引 */
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
/** 笔记修改日期列索引 */
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
/**
*
* <p>
*
* </p>
*
* @param context
* @param folderId ID
*/
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
/**
*
* <p>
*
* </p>
*
* @param context
* @param noteId ID
* @param folderId ID
*/
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
loadNote();
}
/**
*
* <p>
* Widget
* </p>
*/
private void loadNote() {
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
}
cursor.close();
} else {
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData();
}
/**
*
* <p>
*
* </p>
*/
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
}, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
// 加载文本笔记数据
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
// 加载通话记录数据
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext());
}
cursor.close();
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
/**
*
* <p>
*
* </p>
*
* @param context
* @param folderId ID
* @param widgetId Widget ID
* @param widgetType Widget
* @param defaultBgColorId ID
* @return WorkingNote
*/
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
return note;
}
/**
*
* <p>
* ID
* </p>
*
* @param context
* @param id ID
* @return WorkingNote
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
*
* <p>
*
*
* Widget Widget
* </p>
*
* @return true false
*/
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
// 创建新笔记
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
// 同步笔记数据
mNote.syncNote(mContext, mNoteId);
/**
* Widget Widget
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return true;
} else {
return false;
}
}
/**
*
*
* @return ID 0 true false
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
*
* <p>
*
* </p>
*
* @return true false
*/
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
return false;
} else {
return true;
}
}
/**
*
*
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
*
* <p>
*
* </p>
*
* @param date
* @param set
*/
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
/**
*
* <p>
* Widget
* </p>
*
* @param mark
*/
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
/**
* ID
* <p>
*
* </p>
*
* @param id ID
*/
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
/**
*
* <p>
*
* </p>
*
* @param mode
*/
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
/**
* Widget
*
* @param type Widget
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
/**
* Widget ID
*
* @param id Widget ID
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
/**
*
* <p>
*
* </p>
*
* @param text
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
/**
*
* <p>
*
* </p>
*
* @param phoneNumber
* @param callDate
*/
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
/**
*
*
* @return true false
*/
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
/**
*
*
* @return
*/
public String getContent() {
return mContent;
}
/**
*
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
*
* @return ID
*/
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
/**
* ID
*
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
*
* @return ID
*/
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
/**
*
*
* @return
*/
public int getCheckListMode() {
return mMode;
}
/**
* ID
*
* @return ID
*/
public long getNoteId() {
return mNoteId;
}
/**
* ID
*
* @return ID
*/
public long getFolderId() {
return mFolderId;
}
/**
* Widget ID
*
* @return Widget ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
* Widget
*
* @return Widget
*/
public int getWidgetType() {
return mWidgetType;
}
/**
*
* <p>
* UI
* </p>
*/
public interface NoteSettingChangedListener {
/**
*
*/
void onBackgroundColorChanged();
/**
*
*
* @param date
* @param set
*/
void onClockAlertChanged(long date, boolean set);
/**
* Widget
*/
void onWidgetChanged();
/**
*
*
* @param oldMode
* @param newMode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
}

@ -0,0 +1,460 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
/**
*
* <p>
*
* SD
* 使
* </p>
*/
public class BackupUtils {
/** 日志标签 */
private static final String TAG = "BackupUtils";
// Singleton stuff
/** 单例实例 */
private static BackupUtils sInstance;
/**
*
*
* @param context
* @return
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
/**
*
* <p>
*
* </p>
*/
// Currently, the sdcard is not mounted
/** SD 卡未挂载 */
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// The backup file not exist
/** 备份文件不存在 */
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// The data is not well formated, may be changed by other programs
/** 数据格式损坏,可能被其他程序修改 */
public static final int STATE_DATA_DESTROIED = 2;
// Some run-time exception which causes restore or backup fails
/** 系统错误,运行时异常导致备份或恢复失败 */
public static final int STATE_SYSTEM_ERROR = 3;
// Backup or restore success
/** 备份或恢复成功 */
public static final int STATE_SUCCESS = 4;
/** 文本导出对象 */
private TextExport mTextExport;
/**
*
*
* @param context
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
/**
*
*
* @return true false
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
*
*
* @return STATE_SD_CARD_UNMOUONTEDSTATE_SYSTEM_ERROR STATE_SUCCESS
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
*
* @return
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
* <p>
*
*
* </p>
*/
private static class TextExport {
/** 笔记查询投影字段 */
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
NoteColumns.SNIPPET,
NoteColumns.TYPE
};
/** 笔记 ID 列索引 */
private static final int NOTE_COLUMN_ID = 0;
/** 笔记修改日期列索引 */
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
/** 笔记摘要列索引 */
private static final int NOTE_COLUMN_SNIPPET = 2;
/** 数据查询投影字段 */
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
/** 数据内容列索引 */
private static final int DATA_COLUMN_CONTENT = 0;
/** 数据 MIME 类型列索引 */
private static final int DATA_COLUMN_MIME_TYPE = 1;
/** 通话日期列索引 */
private static final int DATA_COLUMN_CALL_DATE = 2;
/** 电话号码列索引 */
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
/** 导出文本格式数组 */
private final String [] TEXT_FORMAT;
/** 文件夹名称格式索引 */
private static final int FORMAT_FOLDER_NAME = 0;
/** 笔记日期格式索引 */
private static final int FORMAT_NOTE_DATE = 1;
/** 笔记内容格式索引 */
private static final int FORMAT_NOTE_CONTENT = 2;
/** 应用上下文 */
private Context mContext;
/** 导出文件名 */
private String mFileName;
/** 导出文件目录 */
private String mFileDirectory;
/**
*
*
* @param context
*/
public TextExport(Context context) {
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
mFileName = "";
mFileDirectory = "";
}
/**
*
*
* @param id
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param ps
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// Query notes belong to this folder
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
folderId
}, null);
if (notesCursor != null) {
if (notesCursor.moveToFirst()) {
do {
// Print note's last modified date
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
notesCursor.close();
}
}
/**
*
* <p>
* MIME
* </p>
*
* @param noteId ID
* @param ps
*/
private void exportNoteToText(String noteId, PrintStream ps) {
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {
noteId
}, null);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// Print phone number
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
phoneNumber));
}
// Print call date
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate)));
// Print call attachment location
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
location));
}
} else if (DataConstants.NOTE.equals(mimeType)) {
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
content));
}
}
} while (dataCursor.moveToNext());
}
dataCursor.close();
}
// print a line separator between note
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
});
} catch (IOException e) {
Log.e(TAG, e.toString());
}
}
/**
*
* <p>
*
*
* </p>
*
* @return STATE_SD_CARD_UNMOUONTEDSTATE_SYSTEM_ERROR STATE_SUCCESS
*/
public int exportToText() {
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// First export folder and its notes
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// Print folder's name
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// Export notes in root's folder
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=0", null, null);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
ps.close();
return STATE_SUCCESS;
}
/**
*
* <p>
* SD PrintStream
* </p>
*
* @return PrintStream null
*/
private PrintStream getExportToTextPrintStream() {
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (NullPointerException e) {
e.printStackTrace();
return null;
}
return ps;
}
}
/**
* SD
* <p>
*
* </p>
*
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return null
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
try {
if (!filedir.exists()) {
// 创建目录
filedir.mkdir();
}
if (!file.exists()) {
// 创建文件
file.createNewFile();
}
return file;
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

@ -0,0 +1,439 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
/**
*
* <p>
*
*
* </p>
*/
public class DataUtils {
/** 日志标签 */
public static final String TAG = "DataUtils";
/**
*
* <p>
* ID
*
* </p>
*
* @param resolver ContentResolver
* @param ids ID
* @return true false
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
if (ids.size() == 0) {
Log.d(TAG, "no id is in the hashset");
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
// 跳过系统根文件夹
Log.e(TAG, "Don't delete system folder root");
continue;
}
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
*
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param id ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
* <p>
*
* </p>
*
* @param resolver ContentResolver
* @param ids ID
* @param folderId ID
* @return true false
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
*
* <p>
*
*
* </p>
*
* @param resolver ContentResolver
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
null);
int count = 0;
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
cursor.close();
}
}
}
return count;
}
/**
*
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param noteId ID
* @param type
* @return true false
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
new String [] {String.valueOf(type)},
null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
*
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param noteId ID
* @return true false
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
*
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param dataId ID
* @return true false
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
*
* <p>
*
* </p>
*
* @param resolver ContentResolver
* @param name
* @return true false
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
if(cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exist;
}
/**
* Widget
* <p>
* Widget
* </p>
*
* @param resolver ContentResolver
* @param folderId ID
* @return Widget null
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(folderId) },
null);
HashSet<AppWidgetAttribute> set = null;
if (c != null) {
if (c.moveToFirst()) {
set = new HashSet<AppWidgetAttribute>();
do {
try {
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0);
widget.widgetType = c.getInt(1);
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
}
} while (c.moveToNext());
}
c.close();
}
return set;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param noteId ID
* @return
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE },
null);
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
cursor.close();
}
}
return "";
}
/**
* ID
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return ID 0
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
}
cursor.close();
}
return 0;
}
/**
* ID
* <p>
* ID
* </p>
*
* @param resolver ContentResolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
new String [] { String.valueOf(noteId)},
null);
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0);
}
cursor.close();
return snippet;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
*
* <p>
*
* </p>
*
* @param snippet
* @return
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
// 去除首尾空格
snippet = snippet.trim();
// 截取到第一个换行符之前的内容
int index = snippet.indexOf('\n');
if (index != -1) {
snippet = snippet.substring(0, index);
}
}
return snippet;
}
}

@ -0,0 +1,165 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
/**
* Google Tasks
* <p>
* Google Tasks JSON
*
* </p>
*/
public class GTaskStringUtils {
/** 操作 ID */
public final static String GTASK_JSON_ACTION_ID = "action_id";
/** 操作列表 */
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/** 操作类型 */
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
/** 创建操作类型 */
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
/** 获取所有操作类型 */
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
/** 移动操作类型 */
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
/** 更新操作类型 */
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
/** 创建者 ID */
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/** 子实体 */
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** 客户端版本 */
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
/** 完成状态 */
public final static String GTASK_JSON_COMPLETED = "completed";
/** 当前列表 ID */
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/** 默认列表 ID */
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/** 删除标记 */
public final static String GTASK_JSON_DELETED = "deleted";
/** 目标列表 */
public final static String GTASK_JSON_DEST_LIST = "dest_list";
/** 目标父节点 */
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/** 目标父节点类型 */
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
/** 实体增量 */
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/** 实体类型 */
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
/** 获取已删除标记 */
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
/** ID */
public final static String GTASK_JSON_ID = "id";
/** 索引 */
public final static String GTASK_JSON_INDEX = "index";
/** 最后修改时间 */
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/** 最新同步点 */
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
/** 列表 ID */
public final static String GTASK_JSON_LIST_ID = "list_id";
/** 列表集合 */
public final static String GTASK_JSON_LISTS = "lists";
/** 名称 */
public final static String GTASK_JSON_NAME = "name";
/** 新 ID */
public final static String GTASK_JSON_NEW_ID = "new_id";
/** 笔记集合 */
public final static String GTASK_JSON_NOTES = "notes";
/** 父节点 ID */
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/** 前一个兄弟节点 ID */
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
/** 结果集合 */
public final static String GTASK_JSON_RESULTS = "results";
/** 源列表 */
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/** 任务集合 */
public final static String GTASK_JSON_TASKS = "tasks";
/** 类型 */
public final static String GTASK_JSON_TYPE = "type";
/** 分组类型 */
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/** 任务类型 */
public final static String GTASK_JSON_TYPE_TASK = "TASK";
/** 用户信息 */
public final static String GTASK_JSON_USER = "user";
/** MIUI 文件夹前缀 */
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/** 默认文件夹名称 */
public final static String FOLDER_DEFAULT = "Default";
/** 通话记录文件夹名称 */
public final static String FOLDER_CALL_NOTE = "Call_Note";
/** 元数据文件夹名称 */
public final static String FOLDER_META = "METADATA";
/** 元数据 GTask ID 头 */
public final static String META_HEAD_GTASK_ID = "meta_gid";
/** 元数据笔记头 */
public final static String META_HEAD_NOTE = "meta_note";
/** 元数据头 */
public final static String META_HEAD_DATA = "meta_data";
/** 元数据笔记名称 */
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -0,0 +1,321 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.preference.PreferenceManager;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
/**
*
* <p>
* Widget
*
* </p>
*/
public class ResourceParser {
/** 黄色背景 */
public static final int YELLOW = 0;
/** 蓝色背景 */
public static final int BLUE = 1;
/** 白色背景 */
public static final int WHITE = 2;
/** 绿色背景 */
public static final int GREEN = 3;
/** 红色背景 */
public static final int RED = 4;
/** 默认背景颜色 */
public static final int BG_DEFAULT_COLOR = YELLOW;
/** 小号字体 */
public static final int TEXT_SMALL = 0;
/** 中号字体 */
public static final int TEXT_MEDIUM = 1;
/** 大号字体 */
public static final int TEXT_LARGE = 2;
/** 超大号字体 */
public static final int TEXT_SUPER = 3;
/** 默认字体大小 */
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
*
* <p>
*
*
* </p>
*/
public static class NoteBgResources {
/** 编辑区域背景资源数组 */
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
R.drawable.edit_white,
R.drawable.edit_green,
R.drawable.edit_red
};
/** 标题栏背景资源数组 */
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
};
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
/**
* ID
* <p>
*
* ID
* </p>
*
* @param context
* @return ID0-4
*/
public static int getDefaultBgId(Context context) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
// 随机选择背景颜色
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
return BG_DEFAULT_COLOR;
}
}
/**
*
* <p>
*
*
* </p>
*/
public static class NoteItemBgResources {
/** 首项背景资源数组 */
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
};
/** 中间项背景资源数组 */
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
};
/** 末项背景资源数组 */
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
};
/** 单项背景资源数组 */
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
};
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
* ID
*
* @param id ID0-4
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
* ID
*
* @return ID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
/**
* Widget
* <p>
* Widget
* 2x2 4x4 Widget
* </p>
*/
public static class WidgetBgResources {
/** 2x2 Widget 背景资源数组 */
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
};
/**
* 2x2 Widget ID
*
* @param id ID0-4
* @return 2x2 Widget ID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
/** 4x4 Widget 背景资源数组 */
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
};
/**
* 4x4 Widget ID
*
* @param id ID0-4
* @return 4x4 Widget ID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
/**
*
* <p>
*
*
* </p>
*/
public static class TextAppearanceResources {
/** 文本外观样式资源数组 */
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
R.style.TextAppearanceLarge,
R.style.TextAppearanceSuper
};
/**
* ID
* <p>
* ID
* </p>
*
* @param id ID0-3
* @return ID
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: SharedPreferences ID bug
* ID
* {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE;
}
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}
}
}

@ -0,0 +1,260 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.Window;
import android.view.WindowManager;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.io.IOException;
/**
*
*
*
* AlarmReceiver
*
*
* 1.
* 2.
* 3.
* 4.
*
* @see NoteEditActivity
* @see net.micode.notes.tool.DataUtils
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// 当前提醒的笔记ID
private long mNoteId;
// 笔记内容摘要
private String mSnippet;
// 摘要预览最大长度
private static final int SNIPPET_PREW_MAX_LEN = 60;
// 媒体播放器,用于播放闹钟声音
MediaPlayer mPlayer;
/**
*
*
*
*
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 请求无标题窗口
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Window win = getWindow();
// 添加FLAG_SHOW_WHEN_LOCKED标志使活动可以在锁屏界面上显示
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 如果屏幕当前是关闭状态,添加以下标志
if (!isScreenOn()) {
// 保持屏幕常亮
// 打开屏幕
// 允许在屏幕亮起时锁定
// 设置窗口布局包含系统装饰区域
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}
// 获取启动此活动的Intent
Intent intent = getIntent();
try {
// 从Intent中解析出笔记ID
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 通过笔记ID获取笔记内容摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 如果摘要超过最大长度,截取并添加省略号
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
}
// 初始化媒体播放器
mPlayer = new MediaPlayer();
// 检查笔记是否在数据库中存在且可见
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
// 显示操作对话框
showActionDialog();
// 播放闹钟声音
playAlarmSound();
} else {
// 如果笔记不存在,直接关闭活动
finish();
}
}
/**
*
*
* @return truefalse
*/
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
/**
*
*
*
*
*/
private void playAlarmSound() {
// 获取系统默认的闹钟铃声URI
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 获取受静音模式影响的音频流类型
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
// 如果闹钟音频流受静音模式影响,使用受影响的流类型
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
// 否则使用标准闹钟音频流
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
// 设置音频源
mPlayer.setDataSource(this, url);
// 准备播放
mPlayer.prepare();
// 设置循环播放
mPlayer.setLooping(true);
// 开始播放
mPlayer.start();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
*
*
* AlertDialog
* "查看笔记"
*/
private void showActionDialog() {
// 创建AlertDialog构建器
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
// 设置对话框标题为应用名称
dialog.setTitle(R.string.app_name);
// 设置对话框内容为笔记摘要
dialog.setMessage(mSnippet);
// 添加"确定"按钮,点击事件由当前类处理
dialog.setPositiveButton(R.string.notealert_ok, this);
// 如果屏幕是开启状态,添加"查看笔记"按钮
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 显示对话框并设置关闭监听器
dialog.show().setOnDismissListener(this);
}
/**
*
*
*
*
* @param dialog null
* @param which ID DialogInterface.BUTTON_POSITIVE
* DialogInterface.BUTTON_NEGATIVE
*/
public void onClick(DialogInterface dialog, int which) {
switch (which) {
// 如果点击了"查看笔记"按钮(负按钮)
case DialogInterface.BUTTON_NEGATIVE:
// 创建跳转到笔记编辑活动的Intent
Intent intent = new Intent(this, NoteEditActivity.class);
// 设置动作为查看
intent.setAction(Intent.ACTION_VIEW);
// 传递笔记ID
intent.putExtra(Intent.EXTRA_UID, mNoteId);
// 启动笔记编辑活动
startActivity(intent);
break;
// 默认情况(点击"确定"按钮)
default:
break;
}
}
/**
*
*
*
*
*
* @param dialog null
*/
public void onDismiss(DialogInterface dialog) {
// 停止闹钟声音
stopAlarmSound();
// 关闭当前活动
finish();
}
/**
*
*
*
*/
private void stopAlarmSound() {
if (mPlayer != null) {
// 停止播放
mPlayer.stop();
// 释放资源
mPlayer.release();
// 将播放器对象置空
mPlayer = null;
}
}
}

@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlarmManager; // 系统闹钟管理器,用于设置和管理系统级闹钟
import android.app.PendingIntent; // 延迟意图,用于在指定时间触发操作
import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播
import android.content.ContentUris; // 用于处理内容URI的工具类
import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口
import android.content.Intent; // 意图,用于组件间通信
import android.database.Cursor; // 数据库游标,用于遍历查询结果
import net.micode.notes.data.Notes; // 笔记数据相关类
import net.micode.notes.data.Notes.NoteColumns; // 笔记表列定义
/**
*
*
* BroadcastReceiver
*
*
*
* 1. BOOT_COMPLETED广
* 2.
*/
public class AlarmInitReceiver extends BroadcastReceiver {
/**
*
* ID
*/
private static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE // 提醒日期
};
// 列索引常量,用于从查询结果中获取对应列的数据
private static final int COLUMN_ID = 0; // ID列在结果集中的索引
private static final int COLUMN_ALERTED_DATE = 1; // 提醒日期列在结果集中的索引
/**
* 广
*
* 广广
*
*
* @param context 访
* @param intent 广
*/
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前系统时间,作为查询条件
long currentDate = System.currentTimeMillis();
// 查询所有提醒时间晚于当前时间的笔记
// 查询条件:提醒日期 > 当前时间 AND 笔记类型 = 普通笔记
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION, // 指定查询的列
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE, // 查询条件
new String[] { String.valueOf(currentDate) }, // 查询参数
null); // 排序方式null表示默认排序
// 处理查询结果
if (c != null) {
// 如果有查询结果,遍历所有符合条件的笔记
if (c.moveToFirst()) {
do {
// 获取笔记的提醒时间
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建一个指向AlarmReceiver的Intent用于在闹钟触发时接收广播
Intent sender = new Intent(context, AlarmReceiver.class);
// 将笔记ID作为URI数据附加到Intent中这样AlarmReceiver就能知道是哪个笔记的闹钟触发了
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建PendingIntent它封装了上述Intent可以在指定时间触发
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 获取系统闹钟服务
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 设置闹钟
// 使用RTC_WAKEUP模式即使设备处于睡眠状态也会唤醒设备并触发广播
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 移动到下一条记录
}
// 关闭游标,释放资源
c.close();
}
}
}

@ -0,0 +1,63 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.BroadcastReceiver; // 广播接收器基类,用于接收系统广播
import android.content.Context; // 应用上下文,提供访问应用环境和资源的接口
import android.content.Intent; // 意图,用于组件间通信
/**
*
*
* BroadcastReceiverAlarmManager
* AlarmManager广广
* (AlarmAlertActivity)
*
*
* 1. AlarmInitReceiver
* 2. 广
* 3. AlarmReceiver广AlarmAlertActivity
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
* 广
*
* 广
* IntentAlarmAlertActivityFLAG_ACTIVITY_NEW_TASK
* 使UIActivity
*
* @param context Activity
* @param intent 广IntentID
*/
@Override
public void onReceive(Context context, Intent intent) {
// 将Intent的目标组件设置为AlarmAlertActivity
// 这样当启动Activity时就会显示闹钟提醒界面
intent.setClass(context, AlarmAlertActivity.class);
// 添加FLAG_ACTIVITY_NEW_TASK标志
// 这是必需的因为从非Activity上下文(如BroadcastReceiver)启动Activity时
// 必须指定这个标志,表示启动一个新的任务栈
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动AlarmAlertActivity显示闹钟提醒
// 原始Intent中包含了触发闹钟的笔记ID等信息AlarmAlertActivity会使用这些信息
// 来显示相应的笔记内容和提醒信息
context.startActivity(intent);
}
}

@ -0,0 +1,651 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
/**
*
* <p>
* FrameLayout
* 使NumberPicker/
* 2412
* </p>
* <p>
*
* <ul>
* <li>37</li>
* <li>240-23121-12</li>
* <li>0-59</li>
* <li>/12</li>
* <li></li>
* <li>/</li>
* </ul>
* </p>
*
* @see NumberPicker
* @see OnDateTimeChangedListener
*/
public class DateTimePicker extends FrameLayout {
private static final boolean DEFAULT_ENABLE_STATE = true;
private static final int HOURS_IN_HALF_DAY = 12;
private static final int HOURS_IN_ALL_DAY = 24;
private static final int DAYS_IN_ALL_WEEK = 7;
private static final int DATE_SPINNER_MIN_VAL = 0;
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
private static final int MINUT_SPINNER_MIN_VAL = 0;
private static final int MINUT_SPINNER_MAX_VAL = 59;
private static final int AMPM_SPINNER_MIN_VAL = 0;
private static final int AMPM_SPINNER_MAX_VAL = 1;
private final NumberPicker mDateSpinner;
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
private Calendar mDate;
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
private boolean mIsAm;
private boolean mIs24HourView;
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
private boolean mInitialising;
private OnDateTimeChangedListener mOnDateTimeChangedListener;
/**
*
* <p>
*
* </p>
*/
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 根据选择器的变化调整日期
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
updateDateControl();
onDateTimeChanged();
}
};
/**
*
* <p>
* 230023
* 12/
* </p>
*/
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
// 处理12小时制下的跨日情况
if (!mIs24HourView) {
// 从下午11点变为12点日期加1天
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
// 从12点变为下午11点日期减1天
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
// 切换上午/下午
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
// 处理24小时制下的跨日情况
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 计算新的小时数
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
onDateTimeChanged();
// 如果日期发生变化,更新年月日
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
/**
*
* <p>
* 590059
* </p>
*/
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
// 从最大值变为最小值小时加1
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
// 从最小值变为最大值小时减1
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
// 如果跨小时,更新小时和日期
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
// 更新上午/下午状态
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
/**
* /
* <p>
* //
* </p>
*/
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm;
// 切换上午/下午,调整小时数
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
onDateTimeChanged();
}
};
/**
*
* <p>
*
* </p>
*/
public interface OnDateTimeChangedListener {
/**
*
*
* @param view
* @param year
* @param month 0-11
* @param dayOfMonth 1-31
* @param hourOfDay 0-23
* @param minute 0-59
*/
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
/**
*
*
* 使
* 使24
*
* @param context
*/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
/**
*
*
* 使
* 使24
*
* @param context
* @param date
*/
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
/**
*
*
* 使
* NumberPicker
*
* @param context
* @param date
* @param is24HourView 使24true24false12
*/
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance();
mInitialising = true;
// 判断当前是否为下午
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 加载布局
inflate(context, R.layout.datetime_picker, this);
// 初始化日期选择器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化上午/下午选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 更新控件到初始状态
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置24小时制显示模式
set24HourView(is24HourView);
// 设置当前时间
setCurrentDate(date);
// 设置启用状态
setEnabled(isEnabled());
// 设置内容描述
mInitialising = false;
}
/**
*
*
* NumberPicker
*
* @param enabled truefalse
*/
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
/**
*
*
* @return truefalse
*/
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/**
*
*
* @return
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
*
*
* @param date
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
*
*
* @param year
* @param month 0-11
* @param dayOfMonth 1-31
* @param hourOfDay 0-23
* @param minute 0-59
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
/**
*
*
* @return
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
*
*
* @param year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
return;
}
mDate.set(Calendar.YEAR, year);
updateDateControl();
onDateTimeChanged();
}
/**
*
*
* @return 0-11
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
*
*
* @param month 0-11
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
/**
*
*
* @return 1-31
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
*
*
* @param dayOfMonth 1-31
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
/**
* 24
*
* @return 0-23
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
/**
*
*
* 240-23121-12
*
* @return
*/
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* 24
*
* @param hourOfDay 0-23
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
if (!mIs24HourView) {
// 处理12小时制下的上午/下午状态
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY;
}
}
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
*
*
* @return 0-59
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
*
*
* @param minute 0-59
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
/**
* 24
*
* @return true24false12
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
*
*
* @param is24HourView true使24false使12
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
// 根据显示模式显示或隐藏上午/下午选择器
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
updateHourControl();
setCurrentHour(hour);
updateAmPmControl();
}
/**
*
*
* 37
* "MM.dd EEEE".
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
// 设置为当前日期的前4天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null);
// 生成7天的日期显示值
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues);
// 设置当前选中项为中间项
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate();
}
/**
* /
*
* //
* 24/12
*/
private void updateAmPmControl() {
if (mIs24HourView) {
// 24小时制下隐藏上午/下午选择器
mAmPmSpinner.setVisibility(View.GONE);
} else {
// 12小时制下显示上午/下午选择器
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
/**
*
*
*
* 240-23121-12
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
*
*
* @param callback null
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
/**
*
*
*
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
}

@ -0,0 +1,179 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.util.Calendar;
import net.micode.notes.R;
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
*
* <p>
* AlertDialog
* 使DateTimePicker2412
* </p>
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li>2412</li>
* </ul>
* </p>
*
* @see DateTimePicker
* @see OnDateTimeSetListener
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
private Calendar mDate = Calendar.getInstance();
private boolean mIs24HourView;
private OnDateTimeSetListener mOnDateTimeSetListener;
private DateTimePicker mDateTimePicker;
/**
*
* <p>
*
* </p>
*/
public interface OnDateTimeSetListener {
/**
*
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
*
*
* DateTimePicker
* 使24
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建日期时间选择器组件
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker);
// 设置日期时间变更监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部Calendar对象
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
// 更新对话框标题
updateTitle(mDate.getTimeInMillis());
}
});
// 设置初始日期时间
mDate.setTimeInMillis(date);
// 将秒数清零
mDate.set(Calendar.SECOND, 0);
// 设置选择器当前日期
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置确定按钮
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置取消按钮
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统设置判断是否使用24小时制
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 更新对话框标题
updateTitle(mDate.getTimeInMillis());
}
/**
* 使24
* <p>
* 使24
* true使24false使12
* </p>
*
* @param is24HourView true使24false使12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
* <p>
* OnDateTimeSet
*
* </p>
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
*
*
* mIs24HourView使24
*
* @param date
*/
private void updateTitle(long date) {
// 设置日期时间格式标志
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
// 根据是否24小时制设置相应的格式标志
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
// 格式化日期时间并设置为对话框标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
*
*
* OnDateTimeSet
*
* @param arg0
* @param arg1 ID
*/
public void onClick(DialogInterface arg0, int arg1) {
// 如果设置了监听器,通知监听器用户选择的日期时间
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,109 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
/**
*
* <p>
* PopupMenuButton
*
* </p>
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
public class DropdownMenu {
// 下拉按钮
private Button mButton;
// 弹出菜单
private PopupMenu mPopupMenu;
// 菜单对象
private Menu mMenu;
/**
*
*
* PopupMenu
*
* @param context
* @param button
* @param menuId ID
*/
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button;
// 设置下拉图标背景
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建弹出菜单
mPopupMenu = new PopupMenu(context, mButton);
mMenu = mPopupMenu.getMenu();
// 加载菜单资源
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 设置按钮点击监听器,点击时显示弹出菜单
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
}
});
}
/**
*
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
* ID
*
* @param id ID
* @return null
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
/**
*
*
* @param title
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}

@ -0,0 +1,153 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* <p>
* CursorAdapterListView
*
* </p>
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*
* @see NotesListActivity
*/
public class FoldersListAdapter extends CursorAdapter {
// 数据库查询投影,指定需要从笔记表中获取的列
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 列索引常量,用于从查询结果中获取对应列的数据
public static final int ID_COLUMN = 0;
public static final int NAME_COLUMN = 1;
/**
*
*
*
*
* @param context
* @param c
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
}
/**
*
*
* FolderListItem
*
* @param context
* @param cursor
* @param parent
* @return FolderListItem
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
/**
*
*
*
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) {
// 如果是根文件夹,显示特殊文本;否则显示文件夹名称
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
/**
*
*
* @param context
* @param position 0
* @return
*/
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
/**
*
* <p>
* LinearLayout
* </p>
*/
private class FolderListItem extends LinearLayout {
// 文件夹名称文本视图
private TextView mName;
/**
*
*
*
*
* @param context
*/
public FolderListItem(Context context) {
super(context);
// 加载布局文件
inflate(context, R.layout.folder_list_item, this);
// 获取文件夹名称文本视图
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
*
*
* @param name
*/
public void bind(String name) {
mName.setText(name);
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,342 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;
import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
/**
*
* <p>
* EditText
* NoteEditActivity
* </p>
* <p>
*
* <ul>
* <li>EditText</li>
* <li>URL</li>
* <li></li>
* <li></li>
* </ul>
* </p>
*
* @see NoteEditActivity
*/
public class NoteEditText extends EditText {
// 日志标签
private static final String TAG = "NoteEditText";
// 当前EditText的索引
private int mIndex;
// 删除前的光标位置
private int mSelectionStartBeforeDelete;
// 电话号码URI方案
private static final String SCHEME_TEL = "tel:" ;
// HTTP URI方案
private static final String SCHEME_HTTP = "http:" ;
// 邮件URI方案
private static final String SCHEME_EMAIL = "mailto:" ;
// URI方案与上下文菜单资源ID的映射
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
*
* <p>
* NoteEditActivityEditText
* </p>
*
* @see NoteEditActivity
*/
public interface OnTextViewChangeListener {
/**
*
*
* @param index EditText
* @param text EditText
*/
void onEditTextDelete(int index, String text);
/**
*
*
* @param index EditText
* @param text EditText
*/
void onEditTextEnter(int index, String text);
/**
*
*
* @param index EditText
* @param hasText
*/
void onTextChange(int index, boolean hasText);
}
// 文本视图变更监听器
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
*
*
* @param context
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
/**
* EditText
*
* @param index EditText
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
*
* @param listener
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
*
*
* @param context
* @param attrs XML
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
*
*
* @param context
* @param attrs XML
* @param defStyle
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
*
*
*
*
* @param event
* @return truefalse
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸坐标
int x = (int) event.getX();
int y = (int) event.getY();
// 减去内边距,得到内容区域的坐标
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
// 加上滚动偏移量
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
// 获取触摸点所在的行号
int line = layout.getLineForVertical(y);
// 获取触摸点在行中的字符偏移量
int off = layout.getOffsetForHorizontal(line, x);
// 设置文本选择光标位置
Selection.setSelection(getText(), off);
break;
}
return super.onTouchEvent(event);
}
/**
*
*
*
*
* @param keyCode
* @param event
* @return truefalse
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 如果设置了监听器返回false让onKeyUp处理
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录删除前的光标位置
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
/**
*
*
*
*
* @param keyCode
* @param event
* @return truefalse
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
// 处理删除键
if (mOnTextViewChangeListener != null) {
// 如果光标在开头且不是第一个EditText删除当前EditText
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
// 处理回车键
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
// 获取光标后的文本
String text = getText().subSequence(selectionStart, length()).toString();
// 保留光标前的文本
setText(getText().subSequence(0, selectionStart));
// 通知监听器创建新的EditText
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event);
}
/**
*
*
*
*
* @param focused
* @param direction
* @param previouslyFocusedRect
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) {
// 失去焦点且文本为空
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
*
*
* URL
*
* @param menu
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
// 获取选区的起始和结束位置
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选区内的所有URLSpan
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
// 根据URL类型确定菜单项文本
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加菜单项
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 点击菜单项时打开链接
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
}

@ -0,0 +1,328 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
/**
* 便
* <p>
* List<NoteInfo> ListView
* 便
* </p>
* <p>
* 使 ViewHolder
* </p>
*/
public class NoteInfoAdapter extends BaseAdapter {
private LayoutInflater inflater;
private List<NotesRepository.NoteInfo> notes;
private HashSet<Long> selectedIds;
private OnNoteButtonClickListener buttonClickListener;
private OnNoteItemClickListener itemClickListener;
private OnNoteItemLongClickListener itemLongClickListener;
/**
* 便
*/
public interface OnNoteButtonClickListener {
/**
*
*
* @param position
* @param noteId 便 ID
*/
void onEditButtonClick(int position, long noteId);
}
/**
* 便
*/
public interface OnNoteItemClickListener {
void onNoteItemClick(int position, long noteId);
}
/**
* 便
*/
public interface OnNoteItemLongClickListener {
void onNoteItemLongClick(int position, long noteId);
}
/**
*
*
* @param context
*/
public NoteInfoAdapter(Context context) {
this.inflater = LayoutInflater.from(context);
this.notes = new ArrayList<>();
this.selectedIds = new HashSet<>();
}
/**
* 便
*
* @param notes 便
*/
public void setNotes(List<NotesRepository.NoteInfo> notes) {
this.notes = notes != null ? notes : new ArrayList<>();
notifyDataSetChanged();
}
/**
* 便 ID
* <p>
* ViewModel selectedNoteIds
* Adapter selectedIds
* </p>
*
* @param selectedIds 便 ID
*/
public void setSelectedIds(HashSet<Long> selectedIds) {
if (selectedIds != null && selectedIds != this.selectedIds) {
this.selectedIds.clear();
this.selectedIds.addAll(selectedIds);
notifyDataSetChanged();
} else if (selectedIds == null) {
this.selectedIds.clear();
notifyDataSetChanged();
}
}
/**
* 便 ID
* <p>
* List<Long> HashSet
* </p>
*
* @param selectedIds 便 ID
*/
public void setSelectedIds(List<Long> selectedIds) {
if (selectedIds != null && !selectedIds.isEmpty()) {
this.selectedIds.clear();
this.selectedIds.addAll(selectedIds);
notifyDataSetChanged();
} else {
this.selectedIds.clear();
notifyDataSetChanged();
}
}
/**
* 便 ID
*
* @return 便 ID
*/
public HashSet<Long> getSelectedIds() {
return selectedIds;
}
/**
*
*
* @param noteId 便 ID
*/
public void toggleSelection(long noteId) {
if (selectedIds.contains(noteId)) {
selectedIds.remove(noteId);
} else {
selectedIds.add(noteId);
}
notifyDataSetChanged();
}
/**
*
*
* @param listener
*/
public void setOnNoteButtonClickListener(OnNoteButtonClickListener listener) {
this.buttonClickListener = listener;
}
public void setOnNoteItemClickListener(OnNoteItemClickListener listener) {
this.itemClickListener = listener;
}
public void setOnNoteItemLongClickListener(OnNoteItemLongClickListener listener) {
this.itemLongClickListener = listener;
}
@Override
public int getCount() {
return notes.size();
}
@Override
public Object getItem(int position) {
return position >= 0 && position < notes.size() ? notes.get(position) : null;
}
@Override
public long getItemId(int position) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
return note != null ? note.getId() : -1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("NoteInfoAdapter", "getView called, position: " + position + ", convertView: " + (convertView != null ? "REUSED" : "NEW"));
ViewHolder holder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.note_item, parent, false);
holder = new ViewHolder();
holder.title = convertView.findViewById(R.id.tv_title);
holder.time = convertView.findViewById(R.id.tv_time);
holder.checkBox = convertView.findViewById(android.R.id.checkbox);
holder.pinnedIcon = convertView.findViewById(R.id.iv_pinned_icon);
convertView.setTag(holder);
convertView.setOnClickListener(v -> {
Log.d("NoteInfoAdapter", "===== onClick TRIGGERED =====");
ViewHolder currentHolder = (ViewHolder) v.getTag();
if (currentHolder != null && itemClickListener != null) {
Log.d("NoteInfoAdapter", "Calling itemClickListener");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(currentHolder.position);
if (note != null) {
itemClickListener.onNoteItemClick(currentHolder.position, note.getId());
}
}
Log.d("NoteInfoAdapter", "===== onClick END =====");
});
convertView.setOnLongClickListener(v -> {
Log.d("NoteInfoAdapter", "===== setOnLongClickListener TRIGGERED =====");
Log.d("NoteInfoAdapter", "Event triggered on view: " + v.getClass().getSimpleName());
ViewHolder currentHolder = (ViewHolder) v.getTag();
if (currentHolder != null && itemLongClickListener != null) {
Log.d("NoteInfoAdapter", "Calling itemLongClickListener");
itemLongClickListener.onNoteItemLongClick(currentHolder.position, currentHolder.position < notes.size() ? notes.get(currentHolder.position).getId() : -1);
} else {
Log.e("NoteInfoAdapter", "itemLongClickListener is NULL!");
}
Log.d("NoteInfoAdapter", "===== setOnLongClickListener END =====");
return true;
});
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.position = position;
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
if (note != null) {
String title = note.snippet;
if (title == null || title.trim().isEmpty()) {
title = "无标题";
}
holder.title.setText(title);
holder.time.setText(formatDate(note.modifiedDate));
int bgResId;
int totalCount = getCount();
int bgColorId = note.bgColorId;
if (totalCount == 1) {
bgResId = NoteItemBgResources.getNoteBgSingleRes(bgColorId);
} else if (position == 0) {
bgResId = NoteItemBgResources.getNoteBgFirstRes(bgColorId);
} else if (position == totalCount - 1) {
bgResId = NoteItemBgResources.getNoteBgLastRes(bgColorId);
} else {
bgResId = NoteItemBgResources.getNoteBgNormalRes(bgColorId);
}
convertView.setBackgroundResource(bgResId);
if (selectedIds.contains(note.getId())) {
convertView.setActivated(true);
} else {
convertView.setActivated(false);
}
Log.d("NoteInfoAdapter", "===== Setting checkbox visibility =====");
Log.d("NoteInfoAdapter", "selectedIds.isEmpty(): " + selectedIds.isEmpty());
Log.d("NoteInfoAdapter", "selectedIds.size(): " + selectedIds.size());
Log.d("NoteInfoAdapter", "selectedIds contains note " + note.getId() + ": " + selectedIds.contains(note.getId()));
if (!selectedIds.isEmpty()) {
Log.d("NoteInfoAdapter", "Setting checkbox VISIBLE");
holder.checkBox.setVisibility(View.VISIBLE);
holder.checkBox.setChecked(selectedIds.contains(note.getId()));
holder.checkBox.setClickable(false);
} else {
Log.d("NoteInfoAdapter", "Setting checkbox GONE");
holder.checkBox.setVisibility(View.GONE);
}
Log.d("NoteInfoAdapter", "===== Checkbox visibility set =====");
if (note.isPinned) {
holder.pinnedIcon.setVisibility(View.VISIBLE);
} else {
holder.pinnedIcon.setVisibility(View.GONE);
}
}
return convertView;
}
/**
*
*
* @param timestamp
* @return
*/
private String formatDate(long timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
return sdf.format(new Date(timestamp));
}
/**
* ViewHolder ListView
*/
private static class ViewHolder {
TextView title;
TextView time;
CheckBox checkBox;
ImageView pinnedIcon;
int position;
}
}

@ -0,0 +1,418 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import net.micode.notes.data.Contact;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
/**
*
* <p>
*
* 便访
* </p>
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*
* @see NotesListItem
* @see NotesListAdapter
*/
public class NoteItemData {
// 数据库查询投影,指定需要从笔记表中获取的列
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.TOP, // 新增TOP字段
};
// 列索引常量,用于从查询结果中获取对应列的数据
private static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
private static final int CREATED_DATE_COLUMN = 3;
private static final int HAS_ATTACHMENT_COLUMN = 4;
private static final int MODIFIED_DATE_COLUMN = 5;
private static final int NOTES_COUNT_COLUMN = 6;
private static final int PARENT_ID_COLUMN = 7;
private static final int SNIPPET_COLUMN = 8;
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
private static final int TOP_COLUMN = 12;
// 笔记ID
private long mId;
// 提醒日期
private long mAlertDate;
// 背景颜色ID
private int mBgColorId;
// 创建日期
private long mCreatedDate;
// 是否有附件
private boolean mHasAttachment;
// 修改日期
private long mModifiedDate;
// 笔记数量(用于文件夹)
private int mNotesCount;
// 父文件夹ID
private long mParentId;
// 笔记摘要
private String mSnippet;
// 笔记类型
private int mType;
// 桌面小部件ID
private int mWidgetId;
// 桌面小部件类型
private int mWidgetType;
// 是否置顶
private boolean mIsPinned;
// 联系人名称(用于通话记录)
private String mName;
// 电话号码(用于通话记录)
private String mPhoneNumber;
// 是否为列表最后一项
private boolean mIsLastItem;
// 是否为列表第一项
private boolean mIsFirstItem;
// 是否为列表唯一一项
private boolean mIsOnlyOneItem;
// 是否为文件夹后跟随的单个笔记
private boolean mIsOneNoteFollowingFolder;
// 是否为文件夹后跟随的多个笔记之一
private boolean mIsMultiNotesFollowingFolder;
/**
*
*
*
*
*
* @param context 访
* @param cursor PROJECTION
*/
public NoteItemData(Context context, Cursor cursor) {
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 移除清单项的勾选标记,只保留文本内容
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 读取置顶状态
if (cursor.getColumnCount() > TOP_COLUMN) {
mIsPinned = cursor.getInt(TOP_COLUMN) > 0;
} else {
mIsPinned = false;
}
mPhoneNumber = "";
// 如果是通话记录笔记,获取电话号码和联系人名称
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
if (!TextUtils.isEmpty(mPhoneNumber)) {
mName = Contact.getContact(context, mPhoneNumber);
// 如果找不到联系人,使用电话号码作为名称
if (mName == null) {
mName = mPhoneNumber;
}
}
}
if (mName == null) {
mName = "";
}
// 检查当前项在列表中的位置状态
checkPostion(cursor);
}
/**
*
*
*
*
* @param cursor
*/
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 如果是普通笔记且不是第一项,检查前一项是否为文件夹
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
int position = cursor.getPosition();
if (cursor.moveToPrevious()) {
// 前一项是文件夹或系统文件夹
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 检查文件夹后是否还有更多笔记
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
mIsOneNoteFollowingFolder = true;
}
}
// 移动回原位置
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
/**
*
*
* @return truefalse
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
*
*
* @return truefalse
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
*
* @return truefalse
*/
public boolean isLast() {
return mIsLastItem;
}
/**
*
*
* @return
*/
public String getCallName() {
return mName;
}
/**
*
*
* @return truefalse
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
*
* @return truefalse
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
/**
* ID
*
* @return ID
*/
public long getId() {
return mId;
}
/**
*
*
* @return 0
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
*
* @return
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
*
*
* @return truefalse
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
*
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
*
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
*
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
*
* @return
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* ID
*
* @return IDgetParentId
*/
public long getFolderId () {
return mParentId;
}
/**
*
*
* @return Notes.TYPE_NOTENotes.TYPE_FOLDERNotes.TYPE_SYSTEM
*/
public int getType() {
return mType;
}
/**
*
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* ID
*
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
*
* @return truefalse
*/
public boolean hasAlert() {
return (mAlertDate > 0);
}
/**
*
* @return true
*/
public boolean isPinned() {
return mIsPinned;
}
/**
*
*
* @return truefalse
*/
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
*
*
* NoteItemData
*
* @param cursor TYPE_COLUMN
* @return
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}

@ -0,0 +1,849 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.TextUtils;
import android.util.Log;
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;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.PopupMenu;
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;
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.ui.NoteInfoAdapter;
import net.micode.notes.viewmodel.NotesListViewModel;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List;
/**
* Activity
* <p>
* UIViewModel
* MVVM
* </p>
* <p>
* 130570%
* </p>
*
* @see NotesListViewModel
* @see NotesRepository
*/
public class NotesListActivity extends AppCompatActivity
implements NoteInfoAdapter.OnNoteButtonClickListener,
NoteInfoAdapter.OnNoteItemClickListener,
NoteInfoAdapter.OnNoteItemLongClickListener,
SidebarFragment.OnSidebarItemSelectedListener {
private static final String TAG = "NotesListActivity";
private static final int REQUEST_CODE_OPEN_NODE = 102;
private static final int REQUEST_CODE_NEW_NODE = 103;
private NotesListViewModel viewModel;
private ListView notesListView;
private androidx.appcompat.widget.Toolbar toolbar;
private NoteInfoAdapter adapter;
private DrawerLayout drawerLayout;
private FloatingActionButton fabNewNote;
private LinearLayout breadcrumbContainer;
private LinearLayout breadcrumbItems;
// 多选模式状态
private boolean isMultiSelectMode = false;
/**
*
* <p>
* ViewModelUI
* </p>
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用边缘到边缘显示
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
setContentView(R.layout.note_list);
// 处理窗口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;
});
initViewModel();
initViews();
observeViewModel();
}
/**
*
* <p>
*
* </p>
*/
@Override
protected void onStart() {
super.onStart();
viewModel.loadNotes(Notes.ID_ROOT_FOLDER);
}
/**
* ViewModel
*/
private void initViewModel() {
NotesRepository repository = new NotesRepository(getContentResolver());
viewModel = new ViewModelProvider(this,
new ViewModelProvider.Factory() {
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
if (modelClass.isAssignableFrom(NotesListViewModel.class)) {
return (T) new NotesListViewModel(repository);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}).get(NotesListViewModel.class);
Log.d(TAG, "ViewModel initialized");
}
/**
*
*/
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);
// 设置适配器
adapter = new NoteInfoAdapter(this);
notesListView.setAdapter(adapter);
adapter.setOnNoteButtonClickListener(this);
adapter.setOnNoteItemClickListener(this);
adapter.setOnNoteItemLongClickListener(this);
// 设置点击监听
notesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Object item = parent.getItemAtPosition(position);
if (item instanceof NotesRepository.NoteInfo) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) item;
handleItemClick(note, position);
}
}
});
// 初始化 Toolbar
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.app_name);
}
// 初始化为普通模式
updateToolbarForNormalMode();
// 设置 Toolbar 的汉堡菜单按钮点击监听器(打开侧栏)
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
});
// Set FAB click event
fabNewNote = findViewById(R.id.btn_new_note);
if (fabNewNote != null) {
fabNewNote.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());
startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
});
}
}
/**
*
* <p>
* 便
* </p>
*
* @param note
* @param position
*/
private void handleItemClick(NotesRepository.NoteInfo note, int position) {
if (isMultiSelectMode) {
// 多选模式:切换选中状态
boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId());
viewModel.toggleNoteSelection(note.getId(), !isSelected);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
updateToolbarForMultiSelectMode();
} else {
// 普通模式
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
viewModel.enterFolder(note.getId());
} else {
// 便签:打开编辑器
openNoteEditor(note);
}
}
}
/**
* ViewModelLiveData
*/
private void observeViewModel() {
// 观察笔记列表
viewModel.getNotesLiveData().observe(this, new Observer<List<NotesRepository.NoteInfo>>() {
@Override
public void onChanged(List<NotesRepository.NoteInfo> notes) {
updateAdapter(notes);
}
});
// 观察加载状态
viewModel.getIsLoading().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(Boolean isLoading) {
updateLoadingState(isLoading);
}
});
// 观察错误消息
viewModel.getErrorMessage().observe(this, new Observer<String>() {
@Override
public void onChanged(String message) {
if (message != null && !message.isEmpty()) {
showError(message);
}
}
});
// 观察文件夹路径(用于面包屑导航)
viewModel.getFolderPathLiveData().observe(this, new Observer<List<NotesRepository.NoteInfo>>() {
@Override
public void onChanged(List<NotesRepository.NoteInfo> path) {
updateBreadcrumb(path);
}
});
// 观察侧栏刷新通知
viewModel.getSidebarRefreshNeeded().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(Boolean refreshNeeded) {
if (refreshNeeded != null && refreshNeeded) {
// 通知侧栏刷新
SidebarFragment sidebarFragment = (SidebarFragment) getSupportFragmentManager()
.findFragmentById(R.id.sidebar_fragment);
if (sidebarFragment != null) {
sidebarFragment.refreshFolderTree();
}
// 重置刷新状态
viewModel.getSidebarRefreshNeeded().setValue(false);
}
}
});
}
/**
*
*
* @param path
*/
private void updateBreadcrumb(List<NotesRepository.NoteInfo> path) {
if (breadcrumbItems == null || path == null) {
return;
}
breadcrumbItems.removeAllViews();
for (int i = 0; i < path.size(); i++) {
NotesRepository.NoteInfo folder = path.get(i);
// 如果不是第一个,添加分隔符 " > "
if (i > 0) {
TextView separator = new TextView(this);
separator.setText(" > ");
separator.setTextSize(14);
separator.setTextColor(android.R.color.darker_gray);
breadcrumbItems.addView(separator);
}
// 创建面包屑项
TextView breadcrumbItem = (TextView) getLayoutInflater()
.inflate(R.layout.breadcrumb_item, breadcrumbItems, false);
breadcrumbItem.setText(folder.title);
// 如果是当前文件夹(最后一个),高亮显示且不可点击
if (i == path.size() - 1) {
breadcrumbItem.setTextColor(getColor(R.color.primary_color));
breadcrumbItem.setEnabled(false);
} else {
// 其他层级可以点击跳转
final long targetFolderId = folder.id;
breadcrumbItem.setOnClickListener(v -> viewModel.enterFolder(targetFolderId));
}
breadcrumbItems.addView(breadcrumbItem);
}
}
/**
*
*/
private void updateAdapter(List<NotesRepository.NoteInfo> notes) {
adapter.setNotes(notes);
Log.d(TAG, "Adapter updated with " + notes.size() + " notes");
}
/**
*
*/
private void updateLoadingState(boolean isLoading) {
// TODO: 显示/隐藏进度条
}
/**
*
*/
private void showError(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
/**
*
*/
private void openNoteEditor(NotesRepository.NoteInfo note) {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, note.getParentId());
intent.putExtra(Intent.EXTRA_UID, note.getId());
startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
}
/**
*
*
* @param position
* @param noteId 便 ID
*/
@Override
public void onEditButtonClick(int position, long noteId) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
openNoteEditor(note);
} else {
Log.e(TAG, "Edit button clicked but note is null at position: " + position);
}
}
@Override
public void onNoteItemClick(int position, long noteId) {
Log.d(TAG, "===== onNoteItemClick CALLED =====");
Log.d(TAG, "position: " + position + ", noteId: " + noteId);
if (isMultiSelectMode) {
Log.d(TAG, "Multi-select mode active, toggling selection");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId());
viewModel.toggleNoteSelection(note.getId(), !isSelected);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
// 更新toolbar标题
updateToolbarForMultiSelectMode();
}
Log.d(TAG, "===== onNoteItemClick END (multi-select mode) =====");
} else {
Log.d(TAG, "Normal mode, checking item type");
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
Log.d(TAG, "Folder clicked, entering folder: " + note.getId());
viewModel.enterFolder(note.getId());
} else {
// 便签:打开编辑器
Log.d(TAG, "Note clicked, opening editor");
openNoteEditor(note);
}
}
Log.d(TAG, "===== onNoteItemClick END =====");
}
}
@Override
public void onNoteItemLongClick(int position, long noteId) {
Log.d(TAG, "===== onNoteItemLongClick CALLED =====");
Log.d(TAG, "position: " + position + ", noteId: " + noteId);
if (!isMultiSelectMode) {
Log.d(TAG, "Entering multi-select mode");
enterMultiSelectMode();
viewModel.toggleNoteSelection(noteId, true);
if (adapter != null) {
adapter.setSelectedIds(viewModel.getSelectedNoteIds());
}
updateSelectionState(position, true);
Log.d(TAG, "===== onNoteItemLongClick END =====");
} else {
Log.d(TAG, "Multi-select mode already active, ignoring long click");
}
}
/**
*
*/
private void enterMultiSelectMode() {
isMultiSelectMode = true;
// 隐藏FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.GONE);
}
// 更新toolbar为多选模式
updateToolbarForMultiSelectMode();
}
/**
* 退
*/
private void exitMultiSelectMode() {
isMultiSelectMode = false;
// 显示FAB按钮
if (fabNewNote != null) {
fabNewNote.setVisibility(View.VISIBLE);
}
// 清除选中状态
viewModel.clearSelection();
if (adapter != null) {
adapter.setSelectedIds(new java.util.HashSet<>());
adapter.notifyDataSetChanged();
}
// 更新toolbar为普通模式
updateToolbarForNormalMode();
}
/**
* Toolbar
*/
private void updateToolbarForMultiSelectMode() {
if (toolbar == null) return;
// 设置标题为选中数量
int selectedCount = viewModel.getSelectedCount();
String title = getString(R.string.menu_select_title, selectedCount);
toolbar.setTitle(title);
// 设置导航图标为返回(取消多选)
toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
toolbar.setNavigationOnClickListener(v -> exitMultiSelectMode());
// 移除普通模式的菜单(如果有)
toolbar.getMenu().clear();
// 直接在toolbar上添加操作按钮不在三点菜单中
Menu menu = toolbar.getMenu();
// 删除按钮
MenuItem deleteItem = menu.add(Menu.NONE, R.id.multi_select_delete, 1, getString(R.string.menu_delete));
deleteItem.setIcon(android.R.drawable.ic_menu_delete);
deleteItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// 移动按钮
MenuItem moveItem = menu.add(Menu.NONE, R.id.multi_select_move, 2, getString(R.string.menu_move));
moveItem.setIcon(android.R.drawable.ic_menu_sort_by_size);
moveItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// 置顶按钮
boolean allPinned = viewModel.isAllSelectedPinned();
MenuItem pinItem = menu.add(Menu.NONE, R.id.multi_select_pin, 3, allPinned ? getString(R.string.menu_unpin) : getString(R.string.menu_pin));
// 使用上传图标代替置顶图标,或者如果有合适的资源可以使用
pinItem.setIcon(android.R.drawable.ic_menu_upload);
pinItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
/**
* Toolbar
*/
private void updateToolbarForNormalMode() {
if (toolbar == null) return;
// 设置标题为应用名称
toolbar.setTitle(R.string.app_name);
// 设置导航图标为汉堡菜单
toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size);
toolbar.setNavigationOnClickListener(v -> {
if (drawerLayout != null) {
drawerLayout.openDrawer(findViewById(R.id.sidebar_fragment));
}
});
// 清除多选模式菜单
toolbar.getMenu().clear();
// 添加普通模式菜单(如果需要)
// getMenuInflater().inflate(R.menu.note_list_options, menu);
}
/**
*
*/
private void showDeleteDialog() {
int selectedCount = viewModel.getSelectedCount();
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.alert_message_delete_notes, selectedCount));
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
viewModel.deleteSelectedNotes();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
*
*/
private void showMoveMenu() {
// TODO: 实现文件夹选择逻辑
Toast.makeText(this, "移动功能开发中", Toast.LENGTH_SHORT).show();
}
/**
*
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE) {
viewModel.refreshNotes();
}
}
}
/**
*
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.note_list, menu);
return true;
}
/**
*
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
case R.id.menu_search:
// TODO: 打开搜索对话框
Toast.makeText(this, "搜索功能开发中", Toast.LENGTH_SHORT).show();
return true;
case R.id.menu_new_folder:
// 创建新文件夹
showCreateFolderDialog();
return true;
case R.id.menu_export_text:
// TODO: 导出笔记
Toast.makeText(this, "导出功能开发中", Toast.LENGTH_SHORT).show();
return true;
case R.id.menu_sync:
// TODO: 同步功能
Toast.makeText(this, "同步功能暂不可用", Toast.LENGTH_SHORT).show();
return true;
case R.id.menu_setting:
// TODO: 设置功能
Toast.makeText(this, "设置功能开发中", Toast.LENGTH_SHORT).show();
return true;
// 多选模式菜单项
case R.id.multi_select_delete:
showDeleteDialog();
return true;
case R.id.multi_select_move:
showMoveMenu();
return true;
case R.id.multi_select_pin:
boolean wasPinned = viewModel.isAllSelectedPinned();
viewModel.toggleSelectedNotesPin();
String toastMsg = wasPinned ? getString(R.string.menu_unpin) + "成功" : getString(R.string.menu_pin) + "成功";
Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
*
*/
@Override
public void onCreateContextMenu(android.view.ContextMenu menu, View v, android.view.ContextMenu.ContextMenuInfo menuInfo) {
getMenuInflater().inflate(R.menu.sub_folder, menu);
}
/**
*
*/
@Override
public boolean onContextItemSelected(MenuItem item) {
// TODO: 处理文件夹上下文菜单
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);
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position);
if (note != null) {
Log.d("NotesListActivity", "note ID: " + note.getId());
Log.d("NotesListActivity", "Current selectedIds size before update: " + adapter.getSelectedIds().size());
Log.d("NotesListActivity", "Note already in selectedIds: " + adapter.getSelectedIds().contains(note.getId()));
if (adapter.getSelectedIds().contains(note.getId()) != selected) {
if (selected) {
Log.d("NotesListActivity", "Adding note ID to selectedIds");
adapter.getSelectedIds().add(note.getId());
} else {
Log.d("NotesListActivity", "Removing note ID from selectedIds");
adapter.getSelectedIds().remove(note.getId());
}
Log.d("NotesListActivity", "SelectedIds size after update: " + adapter.getSelectedIds().size());
adapter.notifyDataSetChanged();
Log.d("NotesListActivity", "notifyDataSetChanged() called");
} else {
Log.d("NotesListActivity", "Note selection state unchanged, skipping update");
}
} else {
Log.e("NotesListActivity", "note is NULL at position: " + position);
}
Log.d("NotesListActivity", "===== updateSelectionState END =====");
}
// ==================== SidebarFragment.OnSidebarItemSelectedListener 实现 ====================
@Override
public void onFolderSelected(long folderId) {
// 跳转到指定文件夹
viewModel.enterFolder(folderId);
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
@Override
public void onTrashSelected() {
// TODO: 实现跳转到回收站
Log.d(TAG, "Trash selected");
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
@Override
public void onSyncSelected() {
// TODO: 实现同步功能
Log.d(TAG, "Sync selected");
Toast.makeText(this, "同步功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onLoginSelected() {
// TODO: 实现登录功能
Log.d(TAG, "Login selected");
Toast.makeText(this, "登录功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onExportSelected() {
// TODO: 实现导出功能
Log.d(TAG, "Export selected");
Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onSettingsSelected() {
// TODO: 实现设置功能
Log.d(TAG, "Settings selected");
Toast.makeText(this, "设置功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onCreateFolder() {
// 显示创建文件夹对话框
showCreateFolderDialog();
}
/**
*
*/
private void showCreateFolderDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_create_folder_title);
final EditText input = new EditText(this);
input.setHint(R.string.dialog_create_folder_hint);
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(50)});
builder.setView(input);
builder.setPositiveButton(R.string.menu_create_folder, (dialog, which) -> {
String folderName = input.getText().toString().trim();
if (TextUtils.isEmpty(folderName)) {
Toast.makeText(this, R.string.error_folder_name_empty, Toast.LENGTH_SHORT).show();
return;
}
if (folderName.length() > 50) {
Toast.makeText(this, R.string.error_folder_name_too_long, Toast.LENGTH_SHORT).show();
return;
}
// 创建文件夹
NotesRepository repository = new NotesRepository(getContentResolver());
long parentId = viewModel.getCurrentFolderId();
if (parentId == 0) {
parentId = Notes.ID_ROOT_FOLDER;
}
repository.createFolder(parentId, folderName,
new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long folderId) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, R.string.create_folder_success, Toast.LENGTH_SHORT).show();
// 刷新笔记列表
viewModel.loadNotes(viewModel.getCurrentFolderId());
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, "创建文件夹失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
public void onCloseSidebar() {
// 关闭侧栏
if (drawerLayout != null) {
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
}
}
/**
*
* <p>
* 退
*
*
* </p>
*/
@Override
public void onBackPressed() {
if (isMultiSelectMode) {
// 多选模式:退出多选模式
exitMultiSelectMode();
} else if (drawerLayout != null && drawerLayout.isDrawerOpen(findViewById(R.id.sidebar_fragment))) {
// 侧栏打开:关闭侧栏
drawerLayout.closeDrawer(findViewById(R.id.sidebar_fragment));
} else if (viewModel.getCurrentFolderId() != Notes.ID_ROOT_FOLDER &&
viewModel.getCurrentFolderId() != Notes.ID_CALL_RECORD_FOLDER) {
// 子文件夹:返回上一级
if (!viewModel.navigateUp()) {
// 如果没有导航历史,返回根文件夹
viewModel.loadNotes(Notes.ID_ROOT_FOLDER);
}
} else {
// 根文件夹:最小化应用
moveTaskToBack(true);
}
}
}

@ -0,0 +1,291 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import net.micode.notes.data.Notes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
*
*
* CursorAdapterListView
*
*
*
* 1. NotesListItem
* 2.
* 3. ID
* 4.
*
* @see NotesListItem
* @see NoteItemData
*/
public class NotesListAdapter extends CursorAdapter {
private static final String TAG = "NotesListAdapter";
// 应用上下文
private Context mContext;
// 记录选中状态的Mapkey为位置value为是否选中
private HashMap<Integer, Boolean> mSelectedIndex;
// 笔记总数
private int mNotesCount;
// 是否处于选择模式
private boolean mChoiceMode;
/**
*
*
* ID
*/
public static class AppWidgetAttribute {
// 桌面小部件ID
public int widgetId;
// 桌面小部件类型
public int widgetType;
};
/**
*
*
* Map
*
* @param context null
*/
public NotesListAdapter(Context context) {
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
}
/**
*
*
* @param context
* @param cursor
* @param parent
* @return NotesListItem
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
/**
*
*
*
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor);
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
/**
*
*
* @param position 0
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
/**
*
*
* @return truefalse
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
*
*
* @param mode truefalse退
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
/**
*
*
* @param checked truefalse
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
}
/**
* ID
*
* @return IDHashSet
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
/**
*
*
* @return HashSetnull
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
/**
*
*
* @return 0
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
/**
*
*
* @return truefalse
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
*
* @param position 0
* @return truefalse
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
/**
*
*
*
*/
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
/**
*
*
* @param cursor
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}

@ -0,0 +1,149 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
/**
*
* <p>
* LinearLayout
*
*
* </p>
*/
public class NotesListItem extends LinearLayout {
private ImageView mAlert;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
private NoteItemData mItemData;
private CheckBox mCheckBox;
/**
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
/**
*
* @param context 访
* @param data NoteItemData
* @param choiceMode
* @param checked
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
} else {
mCheckBox.setVisibility(View.GONE);
}
mItemData = data;
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
setBackground(data);
}
/**
*
* @param data NoteItemData
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
/**
*
* @return NoteItemData
*/
public NoteItemData getItemData() {
return mItemData;
}
}

@ -0,0 +1,587 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import android.os.Build; // 用于版本检查
import android.content.Context; // 用于 RECEIVER_NOT_EXPORTED 常量
/**
* Activity
* <p>
* Activity
* <ul>
* <li>Google Tasks</li>
* <li></li>
* <li></li>
* </ul>
* </p>
* <p>
* PreferenceActivity使SharedPreferences
* GTaskReceiver广
* </p>
*/
public class NotesPreferenceActivity extends PreferenceActivity {
/**
* SharedPreferences
*/
public static final String PREFERENCE_NAME = "notes_preferences";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/**
* SharedPreferences
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
/**
* Preference
*/
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
/**
* Intent
*/
private static final String AUTHORITIES_FILTER_KEY = "authorities";
/**
* PreferenceCategory
*/
private PreferenceCategory mAccountCategory;
/**
* 广
*/
private GTaskReceiver mReceiver;
/**
*
*/
private Account[] mOriAccounts;
/**
*
*/
private boolean mHasAddedAccount;
/**
* Activity
* <p>
*
* <ul>
* <li>ActionBar</li>
* <li>preferences.xml</li>
* <li>广</li>
* <li></li>
* </ul>
* </p>
* @param icicle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
//registerReceiver(mReceiver, filter);
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);
}
/**
* Activity
* <p>
* Google
* UI
* </p>
*/
@Override
protected void onResume() {
super.onResume();
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
}
}
refreshUI();
}
/**
* Activity
* <p>
* 广
* </p>
*/
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
* <p>
* Preference
* Preference
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
mAccountCategory.addPreference(accountPref);
}
/**
*
* <p>
*
* <ul>
* <li>"取消同步"</li>
* <li>"立即同步"</li>
* </ul>
*
* </p>
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
/**
* UI
* <p>
*
* </p>
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
*
* <p>
* Google
* "添加账户"
* </p>
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
/**
*
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* Google
* <p>
* AccountManager"com.google"
* </p>
* @return Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
* <p>
* SharedPreferences
* <ul>
* <li></li>
* <li>GTASK_IDSYNC_ID</li>
* </ul>
* </p>
* @param account
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
*
* <p>
* SharedPreferences
* GTASK_IDSYNC_ID
* </p>
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广
* <p>
* GTaskSyncService广UI
* </p>
*/
private class GTaskReceiver extends BroadcastReceiver {
/**
* 广
* <p>
* 广UI
* </p>
* @param context
* @param intent 广Intent
*/
@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));
}
}
}
/**
*
* <p>
* ActionBar
*
* </p>
* @param item
* @return truefalse
*/
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
return false;
}
}
}

@ -0,0 +1,517 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
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.viewmodel.FolderListViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Fragment
* <p>
*
* /
* </p>
*/
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;
// 适配器和数据
private FolderTreeAdapter adapter;
private FolderListViewModel viewModel;
// 单击和双击检测
private long lastClickTime = 0;
private View lastClickedView = null;
private static final long DOUBLE_CLICK_INTERVAL = 300; // 毫秒
// 回调接口
private OnSidebarItemSelectedListener listener;
/**
*
*/
public interface OnSidebarItemSelectedListener {
/**
*
* @param folderId ID
*/
void onFolderSelected(long folderId);
/**
*
*/
void onTrashSelected();
/**
*
*/
void onSyncSelected();
/**
*
*/
void onLoginSelected();
/**
*
*/
void onExportSelected();
/**
*
*/
void onSettingsSelected();
/**
*
*/
void onCreateFolder();
/**
*
*/
void onCloseSidebar();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnSidebarItemSelectedListener) {
listener = (OnSidebarItemSelectedListener) context;
} else {
throw new RuntimeException(context.toString() + " must implement OnSidebarItemSelectedListener");
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(FolderListViewModel.class);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.sidebar_layout, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
setupListeners();
observeViewModel();
}
/**
*
*/
public void refreshFolderTree() {
if (viewModel != null) {
viewModel.loadFolderTree();
}
}
/**
*
*/
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);
// 设置RecyclerView
rvFolderTree.setLayoutManager(new LinearLayoutManager(requireContext()));
adapter = new FolderTreeAdapter(new ArrayList<>(), viewModel);
adapter.setOnFolderItemClickListener(this::handleFolderItemClick);
rvFolderTree.setAdapter(adapter);
}
/**
*
*/
private void setupListeners() {
View view = getView();
if (view == null) return;
// 根文件夹(单击展开/收起,双击跳转)
setupFolderClickListener(tvRootFolder, Notes.ID_ROOT_FOLDER);
// 关闭侧栏
view.findViewById(R.id.btn_close_sidebar).setOnClickListener(v -> {
if (listener != null) {
listener.onCloseSidebar();
}
});
// 创建文件夹
view.findViewById(R.id.btn_create_folder).setOnClickListener(v -> showCreateFolderDialog());
// 菜单项
menuSync.setOnClickListener(v -> {
if (listener != null) {
listener.onSyncSelected();
}
});
menuLogin.setOnClickListener(v -> {
if (listener != null) {
listener.onLoginSelected();
}
});
menuExport.setOnClickListener(v -> {
if (listener != null) {
listener.onExportSelected();
}
});
menuSettings.setOnClickListener(v -> {
if (listener != null) {
listener.onSettingsSelected();
}
});
menuTrash.setOnClickListener(v -> {
if (listener != null) {
listener.onTrashSelected();
}
});
}
/**
* /
*/
private void setupFolderClickListener(View view, long folderId) {
view.setOnClickListener(v -> {
android.util.Log.d(TAG, "setupFolderClickListener: folderId=" + folderId);
long currentTime = System.currentTimeMillis();
if (lastClickedView == view && (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Double click on root folder, jumping to: " + folderId);
// 这是双击,执行跳转
if (listener != null) {
// 根文件夹也可以跳转(回到根)
listener.onFolderSelected(folderId);
}
// 重置双击状态
lastClickTime = 0;
lastClickedView = null;
} else {
android.util.Log.d(TAG, "Single click on root folder, will toggle expand in " + DOUBLE_CLICK_INTERVAL + "ms");
// 可能是单击,延迟处理
lastClickTime = currentTime;
lastClickedView = view;
view.postDelayed(() -> {
// 如果在延迟期间没有发生双击,则执行单击操作(展开/收起)
if (System.currentTimeMillis() - lastClickTime >= DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Toggling root folder expand");
toggleFolderExpand(folderId);
}
}, DOUBLE_CLICK_INTERVAL);
}
});
}
/**
* ViewModel
*/
private void observeViewModel() {
viewModel.getFolderTree().observe(getViewLifecycleOwner(), folderItems -> {
if (folderItems != null) {
adapter.setData(folderItems);
adapter.notifyDataSetChanged();
}
});
viewModel.loadFolderTree();
}
/**
* /
*/
private void toggleFolderExpand(long folderId) {
android.util.Log.d(TAG, "toggleFolderExpand: folderId=" + folderId);
viewModel.toggleFolderExpand(folderId);
}
/**
* /
*/
private void handleFolderItemClick(long folderId) {
android.util.Log.d(TAG, "handleFolderItemClick: folderId=" + folderId);
long currentTime = System.currentTimeMillis();
if (lastClickedFolderId == folderId && (currentTime - lastFolderClickTime) < DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Double click detected, jumping to folder: " + folderId);
// 这是双击,执行跳转
if (listener != null) {
listener.onFolderSelected(folderId);
}
// 重置双击状态
lastFolderClickTime = 0;
lastClickedFolderId = -1;
} else {
android.util.Log.d(TAG, "Single click, will toggle expand in " + DOUBLE_CLICK_INTERVAL + "ms");
// 可能是单击,延迟处理
lastFolderClickTime = currentTime;
lastClickedFolderId = folderId;
new android.os.Handler().postDelayed(() -> {
// 如果在延迟期间没有发生双击,则执行单击操作(展开/收起)
if (System.currentTimeMillis() - lastFolderClickTime >= DOUBLE_CLICK_INTERVAL) {
android.util.Log.d(TAG, "Toggling folder expand: " + folderId);
toggleFolderExpand(folderId);
}
}, DOUBLE_CLICK_INTERVAL);
}
}
// 双击检测专用变量(针对文件夹列表项)
private long lastFolderClickTime = 0;
private long lastClickedFolderId = -1;
/**
*
*/
private void showCreateFolderDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext());
builder.setTitle(R.string.dialog_create_folder_title);
final EditText input = new EditText(requireContext());
input.setHint(R.string.dialog_create_folder_hint);
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(MAX_FOLDER_NAME_LENGTH)});
builder.setView(input);
builder.setPositiveButton(R.string.menu_create_folder, (dialog, which) -> {
String folderName = input.getText().toString().trim();
if (TextUtils.isEmpty(folderName)) {
Toast.makeText(requireContext(), R.string.error_folder_name_empty, Toast.LENGTH_SHORT).show();
return;
}
if (folderName.length() > MAX_FOLDER_NAME_LENGTH) {
Toast.makeText(requireContext(), R.string.error_folder_name_too_long, Toast.LENGTH_SHORT).show();
return;
}
// 创建文件夹
NotesRepository repository = new NotesRepository(requireContext().getContentResolver());
long parentId = viewModel.getCurrentFolderId();
if (parentId == 0) {
parentId = Notes.ID_ROOT_FOLDER;
}
repository.createFolder(parentId, folderName,
new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long folderId) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(), R.string.create_folder_success, Toast.LENGTH_SHORT).show();
// 刷新文件夹列表
viewModel.loadFolderTree();
});
}
}
@Override
public void onError(Exception error) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(),
getString(R.string.error_folder_name_too_long) + ": " + error.getMessage(),
Toast.LENGTH_SHORT).show();
});
}
}
});
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
* FolderTreeAdapter
* /
*/
private static class FolderTreeAdapter extends RecyclerView.Adapter<FolderTreeAdapter.FolderViewHolder> {
private List<FolderTreeItem> folderItems;
private FolderListViewModel viewModel;
private OnFolderItemClickListener folderItemClickListener;
public FolderTreeAdapter(List<FolderTreeItem> folderItems, FolderListViewModel viewModel) {
this.folderItems = folderItems;
this.viewModel = viewModel;
}
public void setData(List<FolderTreeItem> folderItems) {
this.folderItems = folderItems;
}
public void setOnFolderItemClickListener(OnFolderItemClickListener listener) {
this.folderItemClickListener = listener;
}
@NonNull
@Override
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.sidebar_folder_item, parent, false);
return new FolderViewHolder(view, folderItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
FolderTreeItem item = folderItems.get(position);
boolean isExpanded = viewModel != null && viewModel.isFolderExpanded(item.folderId);
holder.bind(item, isExpanded);
}
@Override
public int getItemCount() {
return folderItems.size();
}
static class FolderViewHolder extends RecyclerView.ViewHolder {
private View indentView;
private ImageView ivExpandIcon;
private ImageView ivFolderIcon;
private TextView tvFolderName;
private TextView tvNoteCount;
private FolderTreeItem currentItem;
private final OnFolderItemClickListener folderItemClickListener;
public FolderViewHolder(@NonNull View itemView, OnFolderItemClickListener listener) {
super(itemView);
this.folderItemClickListener = listener;
indentView = itemView.findViewById(R.id.indent_view);
ivExpandIcon = itemView.findViewById(R.id.iv_expand_icon);
ivFolderIcon = itemView.findViewById(R.id.iv_folder_icon);
tvFolderName = itemView.findViewById(R.id.tv_folder_name);
tvNoteCount = itemView.findViewById(R.id.tv_note_count);
}
public void bind(FolderTreeItem item, boolean isExpanded) {
this.currentItem = item;
// 设置缩进
int indent = item.level * 32;
indentView.setLayoutParams(new LinearLayout.LayoutParams(indent, LinearLayout.LayoutParams.MATCH_PARENT));
// 设置展开/收起图标
if (item.hasChildren) {
ivExpandIcon.setVisibility(View.VISIBLE);
ivExpandIcon.setRotation(isExpanded ? 90 : 0);
} else {
ivExpandIcon.setVisibility(View.INVISIBLE);
}
// 设置文件夹名称
tvFolderName.setText(item.name);
// 设置便签数量
tvNoteCount.setText(String.format(itemView.getContext()
.getString(R.string.folder_note_count), item.noteCount));
// 设置点击监听器
itemView.setOnClickListener(v -> {
if (folderItemClickListener != null) {
folderItemClickListener.onFolderClick(item.folderId);
}
});
}
}
}
/**
*
*/
public interface OnFolderItemClickListener {
void onFolderClick(long folderId);
}
/**
* FolderTreeItem
*
*/
public static class FolderTreeItem {
public long folderId;
public String name;
public int level; // 层级0表示顶级
public boolean hasChildren;
public int noteCount;
public FolderTreeItem(long folderId, String name, int level, boolean hasChildren, int noteCount) {
this.folderId = folderId;
this.name = name;
this.level = level;
this.hasChildren = hasChildren;
this.noteCount = noteCount;
}
}
@Override
public void onDetach() {
super.onDetach();
listener = null;
}
}

@ -0,0 +1,305 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.viewmodel;
import android.app.Application;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.ui.SidebarFragment.FolderTreeItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* ViewModel
* <p>
*
*
* </p>
*/
public class FolderListViewModel extends AndroidViewModel {
private static final String TAG = "FolderListViewModel";
private MutableLiveData<List<FolderTreeItem>> folderTreeLiveData;
private NotesDatabaseHelper dbHelper;
private NotesRepository repository;
private long currentFolderId = Notes.ID_ROOT_FOLDER; // 当前文件夹ID
private Set<Long> expandedFolderIds = new HashSet<>(); // 已展开的文件夹ID集合
public FolderListViewModel(@NonNull Application application) {
super(application);
dbHelper = NotesDatabaseHelper.getInstance(application);
repository = new NotesRepository(application.getContentResolver());
folderTreeLiveData = new MutableLiveData<>();
}
/**
* ID
*/
public long getCurrentFolderId() {
return currentFolderId;
}
/**
* ID
*/
public void setCurrentFolderId(long folderId) {
this.currentFolderId = folderId;
}
/**
* /
* @param folderId ID
*/
public void toggleFolderExpand(long folderId) {
android.util.Log.d(TAG, "toggleFolderExpand: folderId=" + folderId);
android.util.Log.d(TAG, "Before toggle, expandedFolders: " + expandedFolderIds);
if (expandedFolderIds.contains(folderId)) {
expandedFolderIds.remove(folderId);
android.util.Log.d(TAG, "Collapsed folder: " + folderId);
} else {
expandedFolderIds.add(folderId);
android.util.Log.d(TAG, "Expanded folder: " + folderId);
}
android.util.Log.d(TAG, "After toggle, expandedFolders: " + expandedFolderIds);
// 重新加载文件夹树
loadFolderTree();
}
/**
*
* @param folderId ID
* @return
*/
public boolean isFolderExpanded(long folderId) {
return expandedFolderIds.contains(folderId);
}
/**
* LiveData
*/
public LiveData<List<FolderTreeItem>> getFolderTree() {
return folderTreeLiveData;
}
/**
*
*/
public void loadFolderTree() {
new Thread(() -> {
List<FolderTreeItem> folderTree = buildFolderTree();
folderTreeLiveData.postValue(folderTree);
}).start();
}
/**
*
* <p>
*
* </p>
* @return
*/
private List<FolderTreeItem> buildFolderTree() {
// 查询所有文件夹(不包括系统文件夹)
List<Map<String, Object>> folders = queryAllFolders();
android.util.Log.d(TAG, "QueryAllFolders returned " + folders.size() + " folders");
// 构建文件夹映射表(方便查找父文件夹)
Map<Long, FolderNode> folderMap = new HashMap<>();
List<FolderNode> rootFolders = new ArrayList<>();
// 创建文件夹节点
for (Map<String, Object> folder : folders) {
long id = (Long) folder.get(NoteColumns.ID);
String name = (String) folder.get(NoteColumns.SNIPPET);
long parentId = (Long) folder.get(NoteColumns.PARENT_ID);
int noteCount = ((Number) folder.get(NoteColumns.NOTES_COUNT)).intValue();
android.util.Log.d(TAG, "Folder: id=" + id + ", name=" + name + ", parentId=" + parentId);
FolderNode node = new FolderNode(id, name, parentId, noteCount);
folderMap.put(id, node);
// 如果是顶级文件夹(父文件夹为根),添加到根列表
if (parentId == Notes.ID_ROOT_FOLDER) {
rootFolders.add(node);
android.util.Log.d(TAG, "Added root folder: " + name);
}
}
android.util.Log.d(TAG, "Root folders count: " + rootFolders.size());
// 构建父子关系
for (FolderNode node : folderMap.values()) {
if (node.parentId != Notes.ID_ROOT_FOLDER) {
FolderNode parent = folderMap.get(node.parentId);
if (parent != null) {
parent.children.add(node);
}
}
}
// 转换为扁平列表用于RecyclerView显示
List<FolderTreeItem> folderTree = new ArrayList<>();
// 检查根文件夹是否展开
boolean rootExpanded = expandedFolderIds.contains(Notes.ID_ROOT_FOLDER);
android.util.Log.d(TAG, "Root expanded: " + rootExpanded);
buildFolderTreeList(rootFolders, folderTree, 0, rootExpanded);
android.util.Log.d(TAG, "Final folder tree size: " + folderTree.size());
return folderTree;
}
/**
*
*
*
* @param nodes
* @param folderTree
* @param level
* @param forceExpandChildren
*/
private void buildFolderTreeList(List<FolderNode> nodes, List<FolderTreeItem> folderTree, int level, boolean forceExpandChildren) {
for (FolderNode node : nodes) {
// 顶级文件夹始终显示level=0
// 移除了之前的条件判断,让所有顶级文件夹都能显示
folderTree.add(new FolderTreeItem(
node.id,
node.name,
level,
!node.children.isEmpty(),
node.noteCount
));
// 只有当父文件夹在 expandedFolderIds 中时,才递归处理子文件夹
// 有子节点(!node.children.isEmpty())才检查展开状态
if (!node.children.isEmpty() && expandedFolderIds.contains(node.id)) {
buildFolderTreeList(node.children, folderTree, level + 1, false);
}
}
}
/**
*
* @return
*/
private List<Map<String, Object>> queryAllFolders() {
List<Map<String, Object>> folders = new ArrayList<>();
// 查询所有文件夹类型的笔记
String selection = NoteColumns.TYPE + " = ?";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_FOLDER)
};
Cursor cursor = null;
try {
cursor = dbHelper.getReadableDatabase().query(
TABLE.NOTE,
null,
selection,
selectionArgs,
null,
null,
NoteColumns.MODIFIED_DATE + " DESC"
);
android.util.Log.d(TAG, "Query executed, cursor: " + (cursor != null ? cursor.getCount() : "null"));
if (cursor != null) {
android.util.Log.d(TAG, "Column names: " + java.util.Arrays.toString(cursor.getColumnNames()));
while (cursor.moveToNext()) {
Map<String, Object> folder = new HashMap<>();
long id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
String name = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
// 尝试获取parent_id可能列名不对
int parentIdIndex = cursor.getColumnIndex(NoteColumns.PARENT_ID);
long parentId = -1;
if (parentIdIndex != -1) {
parentId = cursor.getLong(parentIdIndex);
}
// 尝试获取notes_count
int notesCountIndex = cursor.getColumnIndex(NoteColumns.NOTES_COUNT);
int noteCount = 0;
if (notesCountIndex != -1) {
noteCount = cursor.getInt(notesCountIndex);
}
android.util.Log.d(TAG, "Folder data: id=" + id + ", name=" + name + ", parentId=" + parentId + ", noteCount=" + noteCount);
folder.put(NoteColumns.ID, id);
folder.put(NoteColumns.SNIPPET, name);
folder.put(NoteColumns.PARENT_ID, parentId);
folder.put(NoteColumns.NOTES_COUNT, noteCount);
folders.add(folder);
}
}
} catch (Exception e) {
android.util.Log.e(TAG, "Error querying folders", e);
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
return folders;
}
/**
* FolderNode
*
*/
private static class FolderNode {
public long id;
public String name;
public long parentId;
public int noteCount;
public List<FolderNode> children = new ArrayList<>();
public FolderNode(long id, String name, long parentId, int noteCount) {
this.id = id;
this.name = name;
this.parentId = parentId;
this.noteCount = noteCount;
}
}
}

@ -0,0 +1,584 @@
/*
* Copyright (c) 2025, Modern Notes Project
*
* 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.viewmodel;
import android.util.Log;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* ViewModel
* <p>
* UIActivity
*
* </p>
*
* @see NotesRepository
* @see Note
*/
public class NotesListViewModel extends ViewModel {
private static final String TAG = "NotesListViewModel";
private final NotesRepository repository;
// 笔记列表LiveData
private final MutableLiveData<List<NotesRepository.NoteInfo>> notesLiveData = new MutableLiveData<>();
// 加载状态LiveData
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
// 错误消息LiveData
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
// 选中的笔记ID集合
private final HashSet<Long> selectedNoteIds = new HashSet<>();
// 当前文件夹ID
private long currentFolderId = Notes.ID_ROOT_FOLDER;
// 文件夹路径LiveData用于面包屑导航
private final MutableLiveData<List<NotesRepository.NoteInfo>> folderPathLiveData = new MutableLiveData<>();
// 侧栏刷新通知LiveData删除等操作后通知侧栏刷新
private final MutableLiveData<Boolean> sidebarRefreshNeeded = new MutableLiveData<>(false);
// 文件夹导航历史(用于返回上一级)
private final List<Long> folderHistory = new ArrayList<>();
/**
*
*
* @param repository
*/
public NotesListViewModel(NotesRepository repository) {
this.repository = repository;
Log.d(TAG, "ViewModel created");
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<List<NotesRepository.NoteInfo>> getNotesLiveData() {
return notesLiveData;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<Boolean> getIsLoading() {
return isLoading;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<String> getErrorMessage() {
return errorMessage;
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID{@link Notes#ID_ROOT_FOLDER}
*/
public void loadNotes(long folderId) {
this.currentFolderId = folderId;
isLoading.postValue(true);
errorMessage.postValue(null);
// 加载文件夹路径
repository.getFolderPath(folderId, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> path) {
folderPathLiveData.postValue(path);
}
@Override
public void onError(Exception error) {
Log.e(TAG, "Failed to load folder path", error);
}
});
// 加载笔记
repository.getNotes(folderId, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> notes) {
isLoading.postValue(false);
notesLiveData.postValue(notes);
Log.d(TAG, "Successfully loaded " + notes.size() + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "加载笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*/
public void refreshNotes() {
loadNotes(currentFolderId);
}
/**
*
* <p>
*
* </p>
*/
public void createNote() {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.createNote(currentFolderId, new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long noteId) {
isLoading.postValue(false);
Log.d(TAG, "Successfully created note with ID: " + noteId);
refreshNotes();
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "创建笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
*/
public void deleteNote(long noteId) {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.deleteNote(noteId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.remove(noteId);
refreshNotes();
Log.d(TAG, "Successfully deleted note: " + noteId);
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "删除笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*/
public void deleteSelectedNotes() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要删除的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.deleteNotes(noteIds, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully deleted " + rowsAffected + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "批量删除失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param keyword
*/
public void searchNotes(String keyword) {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.searchNotes(keyword, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> notes) {
isLoading.postValue(false);
notesLiveData.postValue(notes);
Log.d(TAG, "Search returned " + notes.size() + " results");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "搜索失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*
* @param noteId ID
* @param selected
*/
public void toggleNoteSelection(long noteId, boolean selected) {
if (selected) {
selectedNoteIds.add(noteId);
} else {
selectedNoteIds.remove(noteId);
}
}
/**
*
* <p>
*
* </p>
*/
public void selectAllNotes() {
List<NotesRepository.NoteInfo> notes = notesLiveData.getValue();
if (notes != null) {
for (NotesRepository.NoteInfo note : notes) {
selectedNoteIds.add(note.getId());
}
}
}
/**
*
* <p>
*
* </p>
*/
public void deselectAllNotes() {
selectedNoteIds.clear();
}
/**
*
*
* @return true
*/
public boolean isAllSelected() {
List<NotesRepository.NoteInfo> notes = notesLiveData.getValue();
if (notes == null || notes.isEmpty()) {
return false;
}
return notes.size() == selectedNoteIds.size();
}
/**
*
*
* @return
*/
public int getSelectedCount() {
return selectedNoteIds.size();
}
/**
* ID
*
* @return ID
*/
public List<Long> getSelectedNoteIds() {
return new ArrayList<>(selectedNoteIds);
}
/**
* ID
*
* @return ID
*/
public long getCurrentFolderId() {
return currentFolderId;
}
/**
*
*
* @param folderId ID
*/
public void setCurrentFolderId(long folderId) {
this.currentFolderId = folderId;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<List<NotesRepository.NoteInfo>> getFolderPathLiveData() {
return folderPathLiveData;
}
/**
* LiveData
*
* @return LiveData
*/
public MutableLiveData<Boolean> getSidebarRefreshNeeded() {
return sidebarRefreshNeeded;
}
/**
*
*/
public void triggerSidebarRefresh() {
sidebarRefreshNeeded.postValue(true);
}
/**
*
*
* @param folderId ID
*/
public void enterFolder(long folderId) {
// 将当前文件夹添加到历史记录
if (currentFolderId != Notes.ID_ROOT_FOLDER && currentFolderId != Notes.ID_CALL_RECORD_FOLDER) {
folderHistory.add(currentFolderId);
}
loadNotes(folderId);
}
/**
*
*
* @return
*/
public boolean navigateUp() {
if (!folderHistory.isEmpty()) {
long parentFolderId = folderHistory.remove(folderHistory.size() - 1);
loadNotes(parentFolderId);
return true;
}
return false;
}
/**
*
* <p>
* 退
* </p>
*/
public void clearSelection() {
selectedNoteIds.clear();
}
/**
*
* <p>
*
* </p>
*/
public void loadFolders() {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.getFolders(new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> folders) {
isLoading.postValue(false);
notesLiveData.postValue(folders);
Log.d(TAG, "Successfully loaded " + folders.size() + " folders");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "加载文件夹失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param targetFolderId ID
*/
public void moveSelectedNotesToFolder(long targetFolderId) {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要移动的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.moveNotes(noteIds, targetFolderId, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
selectedNoteIds.clear();
refreshNotes();
Log.d(TAG, "Successfully moved " + rowsAffected + " notes");
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "移动笔记失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*/
public void toggleSelectedNotesPin() {
if (selectedNoteIds.isEmpty()) {
errorMessage.postValue("请先选择要操作的笔记");
return;
}
isLoading.postValue(true);
errorMessage.postValue(null);
// 检查当前选中笔记的置顶状态
List<NotesRepository.NoteInfo> allNotes = notesLiveData.getValue();
if (allNotes == null) return;
boolean hasUnpinned = false;
for (NotesRepository.NoteInfo note : allNotes) {
if (selectedNoteIds.contains(note.getId())) {
if (!note.isPinned) {
hasUnpinned = true;
break;
}
}
}
// 如果有未置顶的,则全部置顶;否则全部取消置顶
final boolean newPinState = hasUnpinned;
List<Long> noteIds = new ArrayList<>(selectedNoteIds);
repository.batchTogglePin(noteIds, newPinState, new NotesRepository.Callback<Integer>() {
@Override
public void onSuccess(Integer rowsAffected) {
isLoading.postValue(false);
// 保持选中状态,方便用户查看
refreshNotes();
Log.d(TAG, "Successfully toggled pin state to " + newPinState);
}
@Override
public void onError(Exception error) {
isLoading.postValue(false);
String message = "置顶操作失败: " + error.getMessage();
errorMessage.postValue(message);
Log.e(TAG, message, error);
}
});
}
/**
*
*
* @return true
*/
public boolean isAllSelectedPinned() {
if (selectedNoteIds.isEmpty()) return false;
List<NotesRepository.NoteInfo> allNotes = notesLiveData.getValue();
if (allNotes == null) return false;
for (NotesRepository.NoteInfo note : allNotes) {
if (selectedNoteIds.contains(note.getId())) {
if (!note.isPinned) {
return false;
}
}
}
return true;
}
/**
* ViewModel
* <p>
*
* </p>
*/
@Override
protected void onCleared() {
super.onCleared();
selectedNoteIds.clear();
Log.d(TAG, "ViewModel cleared");
}
}

@ -0,0 +1,132 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.util.Log;
import android.widget.RemoteViews;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
public abstract class NoteWidgetProvider extends AppWidgetProvider {
public static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
private static final String TAG = "NoteWidgetProvider";
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?",
new String[] { String.valueOf(appWidgetIds[i])});
}
}
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null);
}
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
for (int i = 0; i < appWidgetIds.length; i++) {
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
int bgId = ResourceParser.getDefaultBgId(context);
String snippet = "";
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
intent.setAction(Intent.ACTION_VIEW);
} else {
snippet = context.getResources().getString(R.string.widget_havenot_content);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
if (c != null) {
c.close();
}
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
* Generate the pending intent to start host for the widget
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
rv.setTextViewText(R.id.widget_text, snippet);
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
protected abstract int getBgResourceId(int bgId);
protected abstract int getLayoutId();
protected abstract int getWidgetType();
}

@ -0,0 +1,47 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
@Override
protected int getLayoutId() {
return R.layout.widget_2x;
}
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;
}
}

@ -0,0 +1,46 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
protected int getLayoutId() {
return R.layout.widget_4x;
}
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
}

@ -0,0 +1,22 @@
<?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>

@ -0,0 +1,20 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save