zxy020924 1 month ago
parent cbf9e3a5ec
commit 904a8fe391

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CheckStyle-IDEA" serialisationVersion="2">
<checkstyleVersion>10.21.3</checkstyleVersion>
<scanScope>JavaOnly</scanScope>
<copyLibs>true</copyLibs>
<option name="thirdPartyClasspath" />
<option name="activeLocationIds" />
<option name="locations">
<list>
<ConfigurationLocation id="bundled-sun-checks" type="BUNDLED" scope="All" description="Sun Checks">(bundled)</ConfigurationLocation>
<ConfigurationLocation id="bundled-google-checks" type="BUNDLED" scope="All" description="Google Checks">(bundled)</ConfigurationLocation>
</list>
</option>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-12T08:49:41.782965600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Lenovo\.android\avd\Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection>
<targets>
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Lenovo\.android\avd\Medium_Phone.avd" />
</handle>
</Target>
</targets>
</DialogSelection>
</SelectionState>
</selectionStates>
</component>
</project>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="Embedded JDK" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
</project>

@ -0,0 +1 @@
/build

@ -0,0 +1,50 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace 'net.micode.notes'
compileSdk 35
defaultConfig {
applicationId "net.micode.notes"
minSdk 24
targetSdk 35
minSdkVersion 24
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
}
buildToolsVersion '35.0.0'
}
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,43 @@
// 声明当前Java文件所在的包路径包名
package com.example.mi_note;
// 导入Android上下文相关类
import android.content.Context;
// 导入AndroidX测试库的核心测试工具类
import androidx.test.platform.app.InstrumentationRegistry;
// 导入AndroidX测试运行器
import androidx.test.ext.junit.runners.AndroidJUnit4;
// 导入JUnit测试框架注解
import org.junit.Test;
// 导入JUnit测试运行器接口
import org.junit.runner.RunWith;
// 导入JUnit断言方法
import static org.junit.Assert.*;
/**
* Android/
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
// 使用AndroidJUnit4测试运行器
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
// 标识这是一个测试方法
@Test
public void useAppContext() {
// 获取被测应用的上下文对象
// InstrumentationRegistry是测试环境的注册中心
// getInstrumentation()获取当前测试的Instrumentation实例
// getTargetContext()返回被测应用的上下文
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
// 使用断言验证包名是否正确
// 参数1预期值期望的包名
// 参数2实际值从上下文中获取的包名
assertEquals("com.example.mi_note", appContext.getPackageName());
}
}

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* 版权所有 (c) 2010-2011MiCode 开源社区 (www.micode.net)
* 根据 Apache 许可证 2.0 版本("许可证")授权;
* 除非符合许可证的规定,否则不得使用本文件。
* 您可以从以下网址获取许可证副本:
* http://www.apache.org/licenses/LICENSE-2.0
* 除非适用法律要求或书面同意,本软件按"原样"分发,
* 没有任何明示或暗示的保证或条件。
* 详见许可证中规定的权限和限制。
*这是一份标准的Apache许可证2.0版本的开源声明)
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="0.1" >
<uses-sdk android:minSdkVersion="24" />
<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:icon="@drawable/icon_app"
android:label="@string/app_name" >
<activity
android:name=".ui.NotesListActivity"
android:exported="true"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan" >
<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:exported="true"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme" >
<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" />
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:exported="true"
android:label="@string/app_widget2x2" >
<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>
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:exported="true"
android:label="@string/app_widget4x4" >
<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:exported="true"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:exported="true"
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:exported="true"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />
</application>
</manifest>

@ -0,0 +1,105 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明该类所在的包
package net.micode.notes.data;
// 导入android.content 包下的Context类用于访问应用程序资源和类
import android.content.Context;
// 导入android.database 包下的Cursor类用于遍历查询结果集
import android.database.Cursor;
// 导入android.provider.ContactsContract.CommonDataKinds 包下的Phone类用于访问联系人的电话号码相关信息
import android.provider.ContactsContract.CommonDataKinds.Phone;
// 导入android.provider.ContactsContract 包下的Data类用于访问联系人的数据
import android.provider.ContactsContract.Data;
// 导入android.telephony 包下的PhoneNumberUtils类用于处理电话号码相关操作
import android.telephony.PhoneNumberUtils;
// 导入android.util 包下的Log类用于日志记录
import android.util.Log;
// 导入java.util 包下的HashMap类用于存储键值对
import java.util.HashMap;
// 定义一个名为Contact的公共类
public class Contact {
// 声明一个静态的HashMap对象sContactCache用于缓存电话号码和对应的联系人姓名
private static HashMap<String, String> sContactCache;
// 定义一个静态常量TAG用于日志标记
private static final String TAG = "Contact";
// 定义一个静态常量CALLER_ID_SELECTION用于构建查询联系人的SQL选择语句
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 = '+')";
/**
*
* @param context 访
* @param phoneNumber
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
// 检查sContactCache是否为空如果为空则进行初始化
if(sContactCache == null) {
sContactCache = new HashMap<>();
}
// 检查sContactCache中是否已经包含该电话号码
if(sContactCache.containsKey(phoneNumber)) {
// 如果包含,则直接从缓存中获取联系人姓名并返回
return sContactCache.get(phoneNumber);
}
// 替换CALLER_ID_SELECTION中的占位符"+"为电话号码的最小匹配格式
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
// 使用上下文的内容解析器进行查询操作
Cursor cursor = context.getContentResolver().query(
// 查询的内容URI
Data.CONTENT_URI,
// 查询要返回的列,这里只需要联系人的显示名称
new String [] { Phone.DISPLAY_NAME },
// 查询的选择条件
selection,
// 选择条件中的参数
new String[] { phoneNumber },
// 排序规则这里为null表示不排序
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());
// 出现异常则返回null
return null;
} finally {
// 无论是否出现异常,最后都要关闭游标
cursor.close();
}
} else {
// 如果游标为空或无法移动到第一行,记录未找到匹配联系人的日志
Log.d(TAG, "No contact matched with number:" + phoneNumber);
// 返回null表示未找到匹配的联系人
return null;
}
}
}

@ -0,0 +1,280 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义Notes应用的数据库结构及相关常量
package net.micode.notes.data;
// 导入Android的Uri类用于处理内容URI
import android.net.Uri;
// Notes类定义数据库表和字段的常量
public class Notes {
// 内容提供者的Authority标识
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; // 回收站文件夹ID
// Intent的额外数据键名
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date"; // 提醒日期
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id"; // 背景色ID
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id"; // 小部件ID
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type"; // 小部件类型
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id"; // 文件夹ID
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date"; // 通话日期
// 小部件类型常量
public static final int TYPE_WIDGET_INVALIDE = -1; // 无效小部件
public static final int TYPE_WIDGET_2X = 0; // 2x大小小部件
public static final int TYPE_WIDGET_4X = 1; // 4x大小小部件
// 数据常量内部类
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE; // 文本笔记的MIME类型
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE; // 通话笔记的MIME类型
}
/**
* URI
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* URI
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
// 笔记表列名接口
public interface NoteColumns {
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
*
* <P> : TEXT </P>
*/
public static final String SNIPPET = "snippet";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
*
* <P> : INTEGER </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
*
* <P> : INTEGER </P>
*/
public static final String TYPE = "type";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
*
* <P> : INTEGER </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* ID
* <P> : INTEGER </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* GoogleID
* <P> : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String VERSION = "version";
}
// 数据表列名接口
public interface DataColumns {
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* MIME
* <P> : Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* ID
* <P> : INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : TEXT </P>
*/
public static final String CONTENT = "content";
/**
* 1MIME_TYPE
* <P> : INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
* 2MIME_TYPE
* <P> : INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* 3MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* 4MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* 5MIME_TYPE
* <P> : TEXT </P>
*/
public static final String DATA5 = "data5";
}
// 文本笔记数据列
public static final class TextNote implements DataColumns {
/**
*
* <P> : Integer 1: 0: </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"; // 多文本笔记的MIME类型
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note"; // 单文本笔记的MIME类型
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note"); // 文本笔记的URI
}
// 通话笔记数据列
public static final class CallNote implements DataColumns {
/**
*
* <P> : INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;
/**
*
* <P> : TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note"; // 多通话笔记的MIME类型
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note"; // 单通话笔记的MIME类型
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note"); // 通话笔记的URI
}
}

@ -0,0 +1,389 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
*Apache2.0
*/
// 笔记应用的数据库辅助类用于创建和管理SQLite数据库
package net.micode.notes.data;
// 导入所需的Android类
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;
// SQLiteOpenHelper的子类用于管理笔记数据库
public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 数据库名称常量
private static final String DB_NAME = "note.db";
// 数据库版本号常量
private static final int DB_VERSION = 4;
// 内部接口,定义数据库表名
public interface TABLE {
public static final String NOTE = "note"; // 笔记表名
public static final String DATA = "data"; // 数据表名
}
// 日志标签
private static final String TAG = "NotesDatabaseHelper";
// 单例实例
private static NotesDatabaseHelper mInstance;
// 创建笔记表的SQL语句
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," + // ID主键
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 父ID
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒日期
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景色ID
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," + // 小部件ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 小部件类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步ID
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标志
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父ID
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // Google任务ID
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号
")";
// 创建数据表的SQL语句
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," + // ID主键
DataColumns.MIME_TYPE + " TEXT NOT NULL," + // MIME类型
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的笔记ID
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," + // 通用数据列1
DataColumns.DATA2 + " INTEGER," + // 通用数据列2
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 通用数据列3
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 通用数据列4
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 通用数据列5
")";
// 创建数据表note_id索引的SQL语句
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* ID
*/
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";
/**
* ID
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
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";
/**
*
*/
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
// 构造函数
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 创建笔记表
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL); // 执行建表SQL
reCreateNoteTableTriggers(db); // 重建触发器
createSystemFolder(db); // 创建系统文件夹
Log.d(TAG, "note table has been created"); // 日志记录
}
// 重建笔记表触发器
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
// 删除旧触发器
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
// 创建新触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
// 创建系统文件夹
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/*
*
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/*
* ()
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/*
* ()
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/*
*
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
// 创建数据表
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL); // 执行建表SQL
reCreateDataTableTriggers(db); // 重建触发器
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL); // 创建索引
Log.d(TAG, "data table has been created"); // 日志记录
}
// 重建数据表触发器
private void reCreateDataTableTriggers(SQLiteDatabase db) {
// 删除旧触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 创建新触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
// 获取单例实例(线程安全)
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
// 数据库创建时的回调方法
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db); // 创建笔记表
createDataTable(db); // 创建数据表
}
// 数据库升级时的回调方法
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false; // 是否需要重建触发器
boolean skipV2 = false; // 是否跳过V2升级
// 从版本1升级到版本2
if (oldVersion == 1) {
upgradeToV2(db); // 执行V2升级
skipV2 = true; // 此次升级包含从V2到V3的升级
oldVersion++; // 版本号递增
}
// 从版本2升级到版本3(如果不需要跳过)
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db); // 执行V3升级
reCreateTriggers = true; // 需要重建触发器
oldVersion++; // 版本号递增
}
// 从版本3升级到版本4
if (oldVersion == 3) {
upgradeToV4(db); // 执行V4升级
oldVersion++; // 版本号递增
}
// 如果需要重建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db); // 重建笔记表触发器
reCreateDataTableTriggers(db); // 重建数据表触发器
}
// 如果升级未完成,抛出异常
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion + "fails");
}
}
// 升级到版本2的实现
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); // 创建数据表
}
// 升级到版本3的实现
private void upgradeToV3(SQLiteDatabase db) {
// 删除不再使用的触发器
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");
// 添加gtask_id列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''");
// 添加回收站系统文件夹
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);
}
// 升级到版本4的实现
private void upgradeToV4(SQLiteDatabase db) {
// 添加version列
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0");
}
}

@ -0,0 +1,314 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 笔记内容提供者的实现类提供对笔记数据的CRUD操作
package net.micode.notes.data;
// 导入所需的Android类
import android.app.SearchManager; // 搜索相关功能
import android.content.ContentProvider; // 内容提供者基类
import android.content.ContentUris; // URI工具类
import android.content.ContentValues; // 键值对存储类
import android.content.Intent; // 意图相关
import android.content.UriMatcher; // URI匹配器
import android.database.Cursor; // 数据库查询结果
import android.database.sqlite.SQLiteDatabase; // SQLite数据库
import android.net.Uri; // URI处理
import android.text.TextUtils; // 文本处理工具
import android.util.Log; // 日志工具
// 导入项目资源
import androidx.annotation.NonNull;
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; // 数据库表名
import java.util.Objects;
// 内容提供者实现类
public class NotesProvider extends ContentProvider {
private static final UriMatcher mMatcher; // URI匹配器用于匹配不同的URI请求
private NotesDatabaseHelper mHelper; // 数据库帮助类
private static final String TAG = "NotesProvider"; // 日志标签
// URI类型常量
private static final int URI_NOTE = 1; // 笔记表操作
private static final int URI_NOTE_ITEM = 2; // 单条笔记操作
private static final int URI_DATA = 3; // 数据表操作
private static final int URI_DATA_ITEM = 4; // 单条数据操作
private static final int URI_SEARCH = 5; // 搜索操作
private static final int URI_SEARCH_SUGGEST = 6; // 搜索建议
// 静态初始化块配置URI匹配规则
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH); // 创建URI匹配器
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // 匹配笔记表URI
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // 匹配单条笔记URI
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // 匹配数据表URI
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // 匹配单条数据URI
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // 匹配搜索URI
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); // 匹配搜索建议URI
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); // 匹配带参数的搜索建议URI
}
/**
* x'0A'SQLite
*
*/
// 搜索结果的投影(列)定义
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," // 笔记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语句
private static final String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE // 从笔记表查询
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?" // 按内容片段模糊匹配
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER // 排除回收站中的笔记
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; // 只查询普通笔记
// 内容提供者创建时调用
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext()); // 获取数据库帮助类实例
return true;
}
// 查询方法
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null; // 查询结果游标
SQLiteDatabase db = mHelper.getReadableDatabase(); // 获取可读数据库
String id = null; // ID变量
switch (mMatcher.match(uri)) { // 根据URI类型处理不同查询
case URI_NOTE: // 查询笔记表
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM: // 查询单条笔记
id = uri.getPathSegments().get(1); // 从URI中获取笔记ID
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA: // 查询数据表
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM: // 查询单条数据
id = uri.getPathSegments().get(1); // 从URI中获取数据ID
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH: // 搜索请求
case URI_SEARCH_SUGGEST: // 搜索建议请求
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null; // 搜索关键字
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) { // 处理搜索建议请求
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1); // 从URI获取搜索词
}
} else { // 处理普通搜索请求
searchString = uri.getQueryParameter("pattern"); // 从查询参数获取搜索模式
}
if (TextUtils.isEmpty(searchString)) { // 搜索词为空则返回空
return null;
}
try {
searchString = String.format("%%%s%%", searchString); // 添加通配符
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY, // 执行原始查询
new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString()); // 记录异常
}
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常
}
if (c != null) {
c.setNotificationUri(Objects.requireNonNull(getContext()).getContentResolver(), uri); // 设置通知URI
}
return c; // 返回查询结果
}
// 插入方法
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库
long dataId = 0, noteId = 0, insertedId = 0; // ID变量
insertedId = switch (mMatcher.match(uri)) { // 根据URI类型处理不同插入
case URI_NOTE -> // 插入笔记
noteId = db.insert(TABLE.NOTE, null, values);
case URI_DATA -> {
if (values.containsKey(DataColumns.NOTE_ID)) { // 检查是否包含笔记ID
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
yield dataId = db.insert(TABLE.DATA, null, values);
}
default -> throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常
};
// 通知笔记URI变更
if (noteId > 0) {
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 通知数据URI变更
if (dataId > 0) {
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
return ContentUris.withAppendedId(uri, insertedId); // 返回插入数据的URI
}
// 删除方法
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
int count = 0; // 删除计数
String id = null; // ID变量
SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库
boolean deleteData = false; // 数据删除标志
switch (mMatcher.match(uri)) { // 根据URI类型处理不同删除
case URI_NOTE: // 删除笔记
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 "; // 只删除ID大于0的笔记
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM: // 删除单条笔记
id = uri.getPathSegments().get(1); // 从URI获取笔记ID
/*
* 0ID
*/
long noteId = Long.parseLong(id);
if (noteId <= 0) { // 系统文件夹不删除
break;
}
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA: // 删除数据
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true; // 标记为数据删除
break;
case URI_DATA_ITEM: // 删除单条数据
id = uri.getPathSegments().get(1); // 从URI获取数据ID
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true; // 标记为数据删除
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常
}
if (count > 0) { // 如果有数据被删除
if (deleteData) { // 如果是数据删除通知笔记URI变更
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(uri, null); // 通知当前URI变更
}
return count; // 返回删除数量
}
// 更新方法
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0; // 更新计数
String id = null; // ID变量
SQLiteDatabase db = mHelper.getWritableDatabase(); // 获取可写数据库
boolean updateData = false; // 数据更新标志
switch (mMatcher.match(uri)) { // 根据URI类型处理不同更新
case URI_NOTE: // 更新笔记表
increaseNoteVersion(-1, selection, selectionArgs); // 增加笔记版本
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM: // 更新单条笔记
id = uri.getPathSegments().get(1); // 从URI获取笔记ID
increaseNoteVersion(Long.parseLong(id), selection, selectionArgs); // 增加笔记版本
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA: // 更新数据表
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true; // 标记为数据更新
break;
case URI_DATA_ITEM: // 更新单条数据
id = uri.getPathSegments().get(1); // 从URI获取数据ID
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true; // 标记为数据更新
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri); // 未知URI异常
}
if (count > 0) { // 如果有数据被更新
if (updateData) { // 如果是数据更新通知笔记URI变更
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
Objects.requireNonNull(getContext()).getContentResolver().notifyChange(uri, null); // 通知当前URI变更
}
return count; // 返回更新数量
}
// 解析选择条件添加AND连接
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
// 增加笔记版本号
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120); // SQL语句构建器
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 "); // 版本号+1
if (id > 0 || !TextUtils.isEmpty(selection)) { // 有条件时添加WHERE
sql.append(" WHERE ");
}
if (id > 0) { // 指定ID的条件
sql.append(NoteColumns.ID + "=").append(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()); // 执行SQL
}
// 获取URI类型未实现
@Override
public String getType(@NonNull Uri uri) {
// TODO Auto-generated method stub
return null;
}
}

@ -0,0 +1,132 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义MetaData类所在的包路径
package net.micode.notes.gtask.data;
// 导入Android数据库Cursor类
import android.database.Cursor;
// 导入Android日志工具类
import android.util.Log;
// 导入应用中自定义的字符串工具类
import net.micode.notes.tool.GTaskStringUtils;
// 导入JSON处理相关类
import org.json.JSONException;
import org.json.JSONObject;
/**
* MetaData - Task
* Google Tasks
*
* 1. GIDGoogle ID
* 2. JSONnotes
* 3.
*/
public class MetaData extends Task {
// 日志标签,使用类名作为标识
private final static String TAG = MetaData.class.getSimpleName();
// 存储关联任务的Google ID
private String mRelatedGid = null;
/**
*
* @param gid Google ID
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 将GID添加到元数据JSON中
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid"); // 记录错误日志
}
// 将JSON对象转为字符串存储到notes字段
setNotes(metaInfo.toString());
// 设置固定名称标识这是一个元数据任务
setName(GTaskStringUtils.META_NOTE_NAME);
}
/**
* Google ID
* @return GIDnull
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
*
* @return notestrue
*/
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
/**
* JSON
* @param js JSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
// 先调用父类方法处理基础字段
super.setContentByRemoteJSON(js);
// 如果notes字段不为空解析其中的GID
if (getNotes() != null) {
try {
// 将notes字符串转为JSON对象
JSONObject metaInfo = new JSONObject(getNotes().trim());
// 从JSON中获取关联GID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
Log.w(TAG, "failed to get related gid"); // 记录警告日志
mRelatedGid = null; // 解析失败时重置GID
}
}
}
/**
* JSON
* @param js JSON
* @throws IllegalAccessError
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 元数据不应从本地JSON设置直接抛出异常
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON
* @throws IllegalAccessError
*/
@Override
public JSONObject getLocalJSONFromContent() {
// 元数据不应生成本地JSON直接抛出异常
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
*
* @param c Cursor
* @throws IllegalAccessError
*/
@Override
public int getSyncAction(Cursor c) {
// 元数据不支持同步动作查询,直接抛出异常
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -0,0 +1,157 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义Node类所在的包路径
package net.micode.notes.gtask.data;
// 导入Android数据库Cursor类用于本地数据操作
import android.database.Cursor;
// 导入JSON处理类
import org.json.JSONObject;
/**
* Node - GTasks
*
* 1.
* 2. GID
* 3.
*/
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任务ID全局唯一标识
private String mGid;
// 节点名称
private String mName;
// 最后修改时间戳(毫秒)
private long mLastModified;
// 删除标记
private boolean mDeleted;
/**
*
*
* - GIDnull
* -
* - 0
* -
*/
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
/**
* JSON
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSON
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* JSON
* @param js JSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSON
* @return JSON
*/
public abstract JSONObject getLocalJSONFromContent();
/**
*
* @param c Cursor
* @return SYNC_ACTION_*
*/
public abstract int getSyncAction(Cursor c);
/**
* GoogleID
* @param gid
*/
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
*/
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
/**
* GoogleID
* @return null
*/
public String getGid() {
return this.mGid;
}
/**
*
* @return
*/
public String getName() {
return this.mName;
}
/**
*
* @return
*/
public long getLastModified() {
return this.mLastModified;
}
/**
*
* @return
*/
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -0,0 +1,258 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义当前类所在的包路径
package net.micode.notes.gtask.data;
// 导入Android内容提供者相关类
import android.content.ContentResolver; // 内容解析器用于访问ContentProvider
import android.content.ContentUris; // 用于构建带有ID的URI
import android.content.ContentValues; // 键值对存储,用于数据库操作
import android.content.Context; // 应用上下文
import android.database.Cursor; // 数据库查询结果集
import android.net.Uri; // 通用资源标识符
import android.os.Build; // 获取Android版本信息
import android.util.Log; // 日志工具
// 导入AndroidX注解版本检查
import androidx.annotation.RequiresApi;
// 导入笔记应用数据相关常量
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;
// 导入JSON处理相关类
import org.json.JSONException; // JSON异常
import org.json.JSONObject; // JSON对象
// 导入Java工具类
import java.util.Objects; // 对象工具类
/**
* SqlData - SQLite
*
* 1.
* 2. JSON
* 3.
*/
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, // 数据ID
DataColumns.MIME_TYPE, // MIME类型
DataColumns.CONTENT, // 内容
DataColumns.DATA1, // 扩展数据1
DataColumns.DATA3 // 扩展数据3
};
// 列索引常量
public static final int DATA_ID_COLUMN = 0; // ID列索引
public static final int DATA_MIME_TYPE_COLUMN = 1; // MIME类型列索引
public static final int DATA_CONTENT_COLUMN = 2; // 内容列索引
public static final int DATA_CONTENT_DATA_1_COLUMN = 3; // 扩展数据1列索引
public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展数据3列索引
// 内容解析器实例
private final ContentResolver mContentResolver;
// 是否为新创建数据的标志
private boolean mIsCreate;
// 数据字段
private long mDataId; // 数据ID
private String mDataMimeType; // MIME类型
private String mDataContent; // 内容
private long mDataContentData1; // 扩展数据1长整型
private String mDataContentData3; // 扩展数据3字符串
// 存储有变动的数据值(用于增量更新)
private final ContentValues mDiffDataValues;
/**
*
* @param context
*/
public SqlData(Context context) {
mContentResolver = context.getContentResolver();
mIsCreate = true; // 标记为新创建
mDataId = INVALID_ID; // 初始化无效ID
mDataMimeType = DataConstants.NOTE; // 默认MIME类型
mDataContent = ""; // 空内容
mDataContentData1 = 0; // 默认扩展数据1
mDataContentData3 = ""; // 空扩展数据3
mDiffDataValues = new ContentValues(); // 初始化变更集合
}
/**
* Cursor
* @param context
* @param c Cursor
*/
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已存在数据
loadFromCursor(c); // 从Cursor加载数据
mDiffDataValues = new ContentValues(); // 初始化变更集合
}
/**
* Cursor
* @param c Cursor
*/
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
* @param js JSON
* @throws JSONException JSON
*/
public void setContent(JSONObject js) throws JSONException {
// 处理数据ID
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
mDataId = dataId;
// 处理MIME类型
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;
// 处理扩展数据1
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
mDataContentData1 = dataContentData1;
// 处理扩展数据3
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
mDataContentData3 = dataContentData3;
}
/**
* JSON
* @return JSON
* @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;
}
// 构建JSON对象
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;
}
/**
*
* @param noteId ID
* @param validateVersion
* @param version validateVersiontrue使
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public void commit(long noteId, boolean validateVersion, long version) {
// 处理新建数据的情况
if (mIsCreate) {
// 如果ID无效且有ID变更移除无效ID
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
// 设置关联笔记ID
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
// 插入新数据
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
// 从返回的URI中解析出新数据的ID
mDataId = Long.parseLong(Objects.requireNonNull(uri).getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e);
throw new ActionFailureException("create note failed");
}
} else {
// 处理更新已有数据的情况
if (!mDiffDataValues.isEmpty()) {
int result;
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,628 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义当前类所在的包路径
package net.micode.notes.gtask.data;
// Android Widget相关类
import android.appwidget.AppWidgetManager; // 管理应用小部件
// Android内容提供者相关类
import android.content.ContentResolver; // 内容解析器用于访问ContentProvider
import android.content.ContentValues; // 键值对存储,用于数据库操作
import android.content.Context; // 应用上下文
// Android数据库相关类
import android.database.Cursor; // 数据库查询结果集
// Android网络相关类
import android.net.Uri; // 通用资源标识符
// Android系统相关类
import android.os.Build; // 获取Android版本信息
import android.util.Log; // 日志工具
// AndroidX注解支持
import androidx.annotation.RequiresApi; // 版本要求注解
// 笔记应用数据相关类
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; // Google Task字符串工具
import net.micode.notes.tool.ResourceParser; // 资源解析工具
// JSON处理相关类
import org.json.JSONArray; // JSON数组处理
import org.json.JSONException; // JSON异常
import org.json.JSONObject; // JSON对象处理
// Java集合类
import java.util.ArrayList; // 动态数组
import java.util.Objects; // 对象工具类
// 定义SqlNote类用于管理笔记数据的数据库操作
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, // 笔记ID
NoteColumns.ALERTED_DATE, // 提醒日期
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建日期
NoteColumns.HAS_ATTACHMENT, // 是否有附件
NoteColumns.MODIFIED_DATE, // 修改日期
NoteColumns.NOTES_COUNT, // 子笔记数量
NoteColumns.PARENT_ID, // 父笔记ID
NoteColumns.SNIPPET, // 内容摘要
NoteColumns.TYPE, // 笔记类型
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
NoteColumns.SYNC_ID, // 同步ID
NoteColumns.LOCAL_MODIFIED, // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID, // 原始父笔记ID
NoteColumns.GTASK_ID, // Google任务ID
NoteColumns.VERSION // 数据版本
};
// 列索引常量定义
public static final int ID_COLUMN = 0; // ID列索引
public static final int ALERTED_DATE_COLUMN = 1; // 提醒日期列索引
public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色列索引
public static final int CREATED_DATE_COLUMN = 3; // 创建日期列索引
public static final int HAS_ATTACHMENT_COLUMN = 4; // 附件标记列索引
public static final int MODIFIED_DATE_COLUMN = 5; // 修改日期列索引
public static final int NOTES_COUNT_COLUMN = 6; // 子笔记数列索引
public static final int PARENT_ID_COLUMN = 7; // 父笔记ID列索引
public static final int SNIPPET_COLUMN = 8; // 摘要列索引
public static final int TYPE_COLUMN = 9; // 类型列索引
public static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引
public static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引
public static final int SYNC_ID_COLUMN = 12; // 同步ID列索引
public static final int LOCAL_MODIFIED_COLUMN = 13; // 本地修改列索引
public static final int ORIGIN_PARENT_ID_COLUMN = 14; // 原始父笔记ID列索引
public static final int GTASK_ID_COLUMN = 15; // Google任务ID列索引
public static final int VERSION_COLUMN = 16; // 版本列索引
// 上下文对象
private final Context mContext;
// 内容解析器
private final ContentResolver mContentResolver;
// 是否为新建笔记标记
private boolean mIsCreate;
// 笔记字段
private long mId; // 笔记ID
private long mAlertDate; // 提醒时间
private int mBgColorId; // 背景颜色ID
private long mCreatedDate; // 创建时间
private int mHasAttachment; // 是否有附件(0/1)
private long mModifiedDate; // 最后修改时间
private long mParentId; // 父笔记ID
private String mSnippet; // 内容摘要
private int mType; // 笔记类型
private int mWidgetId; // 关联小部件ID
private int mWidgetType; // 小部件类型
private long mOriginParent; // 原始父笔记ID
private long mVersion; // 数据版本
// 存储变动的笔记值
private final ContentValues mDiffNoteValues;
// 笔记关联的数据列表
private final ArrayList<SqlData> mDataList;
/**
*
* @param context
*/
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; // 无效小部件ID
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小部件类型
mOriginParent = 0;
mVersion = 0;
mDiffNoteValues = new ContentValues(); // 初始化变更值容器
mDataList = new ArrayList<>(); // 初始化数据列表
}
/**
* Cursor
* @param context
* @param c Cursor
*/
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已有笔记
loadFromCursor(c); // 从Cursor加载数据
mDataList = new ArrayList<>();
// 如果是普通笔记类型,加载关联数据
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
/**
* ID
* @param context
* @param id ID
*/
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已有笔记
loadFromCursor(id); // 通过ID加载笔记
mDataList = new ArrayList<>();
// 如果是普通笔记类型,加载关联数据
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
/**
* ID
* @param id ID
*/
private void loadFromCursor(long id) {
// 使用try-with-resources确保Cursor自动关闭
try (Cursor c = mContentResolver.query(
Notes.CONTENT_NOTE_URI, // 笔记内容URI
PROJECTION_NOTE, // 查询列
"(_id=?)", // 查询条件
new String[]{ String.valueOf(id) }, // 参数
null)) { // 排序
if (c != null) {
c.moveToNext(); // 移动到第一行
loadFromCursor(c); // 从Cursor加载数据
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
}
}
/**
* Cursor
* @param c Cursor
*/
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
mParentId = c.getLong(PARENT_ID_COLUMN);
mSnippet = c.getString(SNIPPET_COLUMN);
mType = c.getInt(TYPE_COLUMN);
mWidgetId = c.getInt(WIDGET_ID_COLUMN);
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
mVersion = c.getLong(VERSION_COLUMN);
}
/**
*
*/
private void loadDataContent() {
mDataList.clear(); // 清空现有数据
// 查询关联数据
try (Cursor c = mContentResolver.query(
Notes.CONTENT_DATA_URI, // 数据内容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");
}
}
}
/**
* JSON
* @param js JSON
* @return
*/
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) {
// 只能更新摘要和类型
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);
// 处理笔记ID
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;
// 处理父笔记ID
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;
// 处理小部件ID
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;
// 处理原始父笔记ID
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;
break;
}
}
}
// 如果不存在则创建新数据
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
* @return JSON
*/
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
* @param id ID
*/
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
/**
* GoogleID
* @param gid GoogleID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* ID
* @param syncId ID
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
*
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
/**
* ID
* @return ID
*/
public long getId() {
return mId;
}
/**
* ID
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
* @return true
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
*
* @param validateVersion
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public void commit(boolean validateVersion) {
// 处理新建笔记
if (mIsCreate) {
// 如果ID无效且有ID变更移除无效ID
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
// 插入新笔记
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
// 从返回的URI中解析出新笔记ID
mId = Long.parseLong(Objects.requireNonNull(uri).getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e);
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 {
// 检查笔记ID有效性
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.isEmpty()) {
mVersion++; // 版本号递增
int result;
// 根据是否验证版本执行不同的更新
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,454 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义当前类所在的包路径
package net.micode.notes.gtask.data;
// Android数据库相关类
import android.database.Cursor; // 数据库查询结果集
// Android文本处理工具
import android.text.TextUtils; // 字符串处理工具类
// Android日志工具
import android.util.Log; // 日志记录工具
// 笔记应用数据相关常量
import androidx.annotation.NonNull;
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; // 同步操作失败异常
// Google Task工具类
import net.micode.notes.tool.GTaskStringUtils; // Google Task字符串工具
// JSON处理相关类
import org.json.JSONArray; // JSON数组处理
import org.json.JSONException; // JSON异常处理
import org.json.JSONObject; // JSON对象处理
// Java工具类
import java.util.Objects; // 对象工具类
// Task类继承自Node基类表示Google Task中的一个任务项
public class Task extends Node {
// 日志标签
private static final String TAG = Task.class.getSimpleName();
// 任务状态字段
private boolean mCompleted; // 任务是否已完成
private String mNotes; // 任务备注/描述信息
private JSONObject mMetaInfo; // 存储元信息的JSON对象
// 任务关系字段
private Task mPriorSibling; // 前一个兄弟任务(用于排序)
private TaskList mParent; // 所属任务列表
// 默认构造函数
public Task() {
super(); // 调用父类构造函数
mCompleted = false; // 默认未完成
mNotes = null; // 默认无备注
mPriorSibling = null; // 默认无前驱任务
mParent = null; // 默认无父列表
mMetaInfo = null; // 默认无元信息
}
/**
* JSON
* @param actionId ID
* @return JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// 设置动作类型为创建
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 设置任务索引位置
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// 构建任务实体数据
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID
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); // 添加实体数据
// 设置父任务列表ID
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// 设置目标父类型为组
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// 设置列表ID
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// 如果有前驱任务则设置其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("生成任务创建JSON对象失败");
}
return js;
}
/**
* JSON
* @param actionId ID
* @return JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// 设置动作类型为更新
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 设置任务全局ID
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 构建更新数据
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("生成任务更新JSON对象失败");
}
return js;
}
/**
* JSON
* @param js 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));
}
// 设置最后修改时间
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 设置任务名称
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// 设置任务备注
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// 设置删除状态
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// 设置完成状态
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("从JSON获取任务内容失败");
}
}
}
/**
* JSON
* @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: 无可用数据");
}
try {
JSONObject note = Objects.requireNonNull(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, "无效的类型");
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
* @return JSON
*/
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
// 从网页端创建的新任务
if (name == null) {
Log.w(TAG, "笔记内容为空");
return null;
}
// 构建新的JSON结构
return getJsonObject(name);
} else {
// 已同步的任务
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;
}
}
@NonNull
private static JSONObject getJsonObject(String name) throws JSONException {
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;
}
/**
*
* @param metaData
*/
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
// 将元数据中的notes字符串转换为JSON对象
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
/**
*
* @param c Cursor
* @return
*/
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, "笔记元信息似乎已被删除");
return SYNC_ACTION_UPDATE_REMOTE; // 需要从远程更新
}
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "远程笔记ID似乎已被删除");
return SYNC_ACTION_UPDATE_LOCAL; // 需要更新本地
}
// 验证笔记ID是否匹配
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "笔记ID不匹配");
return SYNC_ACTION_UPDATE_LOCAL;
}
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// 本地无修改
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 双方都无更新
return SYNC_ACTION_NONE;
} else {
// 应用远程修改到本地
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// 验证任务ID是否匹配
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "任务ID不匹配");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// 仅本地有修改
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; // 默认返回错误
}
/*
*
* @return true
*/
/**
*
*
* 1.
* 2.
* 3.
*
* @return truefalse
*/
public boolean isWorthSaving() {
return mMetaInfo != null || // 存在元信息
(getName() != null && !getName().trim().isEmpty()) || // 有有效名称
(getNotes() != null && !getNotes().trim().isEmpty()); // 有有效备注
}
/**
*
* @param completed truefalse
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed; // 更新完成状态字段
}
/**
*
* @param notes null
*/
public void setNotes(String notes) {
this.mNotes = notes; // 更新备注字段
}
/**
*
* @param priorSibling null
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling; // 更新前驱任务引用
}
/**
*
* @param parent null
*/
public void setParent(TaskList parent) {
this.mParent = parent; // 更新父列表引用
}
/**
*
* @return truefalse
*/
public boolean getCompleted() {
return this.mCompleted; // 返回当前完成状态
}
/**
*
* @return null
*/
public String getNotes() {
return this.mNotes; // 返回当前备注内容
}
/**
*
* @return null
*/
public Task getPriorSibling() {
return this.mPriorSibling; // 返回前驱任务引用
}
/**
*
* @return
*/
public TaskList getParent() {
return this.mParent; // 返回父列表引用
}
}

@ -0,0 +1,435 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明当前类所在的包路径属于笔记应用的Google任务数据模块
package net.micode.notes.gtask.data;
// 导入Android数据库相关类
import android.database.Cursor; // 用于操作和遍历数据库查询结果集
import android.util.Log; // Android日志工具类
// 导入笔记应用的核心数据类
import net.micode.notes.data.Notes; // 笔记数据常量类
import net.micode.notes.data.Notes.NoteColumns; // 笔记表列名常量
// 导入异常处理类
import net.micode.notes.gtask.exception.ActionFailureException;
// 当Google任务操作失败时抛出的自定义异常
// 导入Google任务字符串处理工具类
import net.micode.notes.tool.GTaskStringUtils;
// 包含Google任务API使用的JSON键名常量
// 导入JSON处理相关类
import org.json.JSONException; // JSON解析异常类
import org.json.JSONObject; // JSON对象操作类
// 导入Java工具类
import java.util.ArrayList; // 动态数组集合
import java.util.Objects; // 对象操作工具类(判空等)
// 继承自Node基类表示Google Tasks中的任务列表
public class TaskList extends Node {
// 日志标签,使用类名作为标识
private static final String TAG = TaskList.class.getSimpleName();
// 任务列表的排序索引
private int mIndex;
// 存储子任务的动态数组
private final ArrayList<Task> mChildren;
// 构造函数
public TaskList() {
super(); // 调用父类Node的构造函数
mChildren = new ArrayList<>(); // 初始化子任务列表
mIndex = 1; // 默认索引从1开始
}
/**
* JSON
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// 设置动作类型为"create"
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 设置任务列表索引
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// 构建实体数据对象
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP); // 实体类型设为"group"
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 添加实体数据
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("生成任务列表创建JSON失败");
}
return js;
}
/**
* JSON
* @param actionId ID
* @return JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// 设置动作类型为"update"
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 设置动作ID
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 设置任务列表全局ID
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 构建更新数据对象
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("生成任务列表更新JSON失败");
}
return js;
}
/**
* JSON
* @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));
}
// 设置最后修改时间戳
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 设置任务列表名称
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("从JSON解析任务列表内容失败");
}
}
}
/**
* JSON
* @param js JSON
*/
public void setContentByLocalJSON(JSONObject js) {
// 检查数据有效性
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: 无可用数据");
return;
}
try {
JSONObject folder = Objects.requireNonNull(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); // 添加MIUI前缀
}
// 处理系统文件夹类型
else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER) {
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
} else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER) {
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE);
} else {
Log.e(TAG, "无效的系统文件夹");
}
} else {
Log.e(TAG, "错误的类型");
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* JSON
* @return JSONnull
*/
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
// 处理文件夹名称移除MIUI前缀
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)) {
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.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;
}
}
/**
*
* @param c Cursor
* @return SYNC_ACTION_*
*/
public int getSyncAction(Cursor c) {
try {
// 检查本地是否有修改
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// 无本地修改时
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
return SYNC_ACTION_NONE; // 双方数据一致
} else {
return SYNC_ACTION_UPDATE_LOCAL; // 需要更新本地
}
} else {
// 有本地修改时验证任务ID
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "任务ID不匹配");
return SYNC_ACTION_ERROR;
}
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();
}
/**
*
* @param task
* @return
*/
public boolean addChildTask(Task task) {
boolean ret = false;
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(task);
if (ret) {
// 设置前驱任务和父列表
task.setPriorSibling(mChildren.get(mChildren.size() - 1));
task.setParent(this);
}
}
return ret;
}
/**
*
* @param task
* @param index
* @return
*/
public boolean addChildTask(Task task, int index) {
// 检查索引有效性
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "添加子任务:无效的索引");
return false;
}
// 检查任务是否已存在
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
mChildren.add(index, task);
// 更新任务链关系
Task preTask = (index != 0) ? mChildren.get(index - 1) : null;
Task afterTask = (index != mChildren.size() - 1) ? mChildren.get(index + 1) : null;
task.setPriorSibling(preTask);
if (afterTask != null) {
afterTask.setPriorSibling(task);
}
}
return true;
}
/**
*
* @param task
* @return
*/
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task);
if (ret) {
// 重置被移除任务的关系
task.setPriorSibling(null);
task.setParent(null);
// 更新剩余任务的关系
if (index < mChildren.size()) {
mChildren.get(index).setPriorSibling(
(index == 0) ? null : mChildren.get(index - 1));
}
}
}
return ret;
}
/**
*
* @param task
* @param index
* @return
*/
public boolean moveChildTask(Task task, int index) {
// 检查索引有效性
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "移动子任务:无效的索引");
return false;
}
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "移动子任务:任务不在列表中");
return false;
}
if (pos == index) {
return true; // 位置未改变
}
// 先移除再添加实现移动
return (removeChildTask(task) && addChildTask(task, index));
}
// 以下是查询方法
/**
* GID
* @param gid ID
* @return null
*/
public Task findChildTaskByGid(String gid) {
for (Task t : mChildren) {
if (t.getGid().equals(gid)) {
return t;
}
}
return null;
}
/**
*
* @param task
* @return -1
*/
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
/**
*
* @param index
* @return null
*/
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: 无效的索引");
return null;
}
return mChildren.get(index);
}
/**
* GID
* @param gid ID
* @return null
*/
public Task getChilTaskByGid(String gid) {
return findChildTaskByGid(gid);
}
/**
*
* @return ArrayList
*/
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
// 以下是索引属性的getter/setter
/**
*
* @param index
*/
public void setIndex(int index) {
this.mIndex = index;
}
/**
*
* @return
*/
public int getIndex() {
return this.mIndex;
}
}

@ -0,0 +1,46 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义该异常类所在的包路径属于Google任务模块的异常处理包
package net.micode.notes.gtask.exception;
import java.io.Serial;
/**
* Google Task
* RuntimeException(unchecked exception)
* GTasks
*/
public class ActionFailureException extends RuntimeException {
// 序列化版本UID用于控制反序列化的版本兼容性
// 保持该值不变可以确保序列化/反序列化的一致性
@Serial
private static final long serialVersionUID = 4425249765923293627L;
/**
*
*/
public ActionFailureException() {
super(); // 调用父类RuntimeException的无参构造
}
/**
*
* @param paramString
*/
public ActionFailureException(String paramString) {
super(paramString); // 调用父类带消息的构造方法
}
/**
*
* @param paramString
* @param paramThrowable Throwable
*/
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable); // 调用父类带cause的构造方法
}
}

@ -0,0 +1,50 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明当前类所在的包路径属于Google任务模块的异常处理包
package net.micode.notes.gtask.exception;
// 导入Java序列化相关注解
import java.io.Serial;
/**
* (checked exception)
* java.lang.Exception
* 使
*/
public class NetworkFailureException extends Exception {
// 序列化版本唯一标识符,使用@Serial注解标记
// 该值用于控制不同版本间的序列化兼容性
@Serial
private static final long serialVersionUID = 2107610287180234136L;
/**
*
*
*/
public NetworkFailureException() {
super(); // 调用父类Exception的无参构造方法
}
/**
*
* @param paramString
* "HTTP 404 Not Found"
*/
public NetworkFailureException(String paramString) {
super(paramString); // 调用父类带错误信息的构造方法
}
/**
*
* @param paramString
* @param paramThrowable (Throwable)
* new NetworkFailureException("请求失败", socketTimeoutEx)
*/
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable); // 调用父类带cause的构造方法
}
}

@ -0,0 +1,156 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义远程同步任务异步处理类
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 android.os.Build;
// 导入AndroidX兼容库注解
import androidx.annotation.RequiresApi;
// 导入应用资源
import net.micode.notes.R;
// 导入界面活动类
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
// 异步任务类用于处理Google任务同步
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
// 定义同步完成回调接口
public interface OnCompleteListener {
void onComplete(); // 同步完成时触发
}
// 上下文对象
private final Context mContext;
// 通知管理器
private final NotificationManager mNotifiManager;
// Google任务管理器实例
private final GTaskManager mTaskManager;
// 完成监听器
private final OnCompleteListener mOnCompleteListener;
// 构造函数
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context; // 初始化上下文
mOnCompleteListener = listener; // 设置完成监听器
// 获取系统通知服务
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
// 获取任务管理器单例
mTaskManager = GTaskManager.getInstance();
}
// 取消同步操作
public void cancelSync() {
mTaskManager.cancelSync(); // 调用任务管理器取消方法
}
// 发布进度更新
public void publishProgess(String message) {
publishProgress(message // 包装成字符串数组
);
}
// 显示通知的方法兼容新版API
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();
// 定义通知ID
int GTASK_SYNC_NOTIFICATION_ID = 5234235;
// 显示通知
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
// 后台执行方法API级别限制
@RequiresApi(api = Build.VERSION_CODES.R)
@Override
protected Integer doInBackground(Void... unused) {
// 发布登录进度通知
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
// 执行同步操作
return mTaskManager.sync(mContext, this);
}
// 进度更新回调
@Override
protected void onProgressUpdate(String... progress) {
// 显示同步中通知
showNotification(R.string.ticker_syncing, progress[0]);
// 如果是服务上下文,发送广播
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
// 执行完成回调
@Override
protected void onPostExecute(Integer result) {
// 根据同步结果显示不同通知
if (result == GTaskManager.STATE_SUCCESS) {
// 同步成功通知
showNotification(R.string.ticker_success, mContext.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
// 记录最后同步时间
NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
// 网络错误通知
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
// 内部错误通知
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
// 取消同步通知
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
// 触发完成回调
if (mOnCompleteListener != null) {
// 在新线程中执行回调
new Thread(mOnCompleteListener::onComplete).start();
}
}
}

@ -0,0 +1,611 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义Google任务远程同步包路径
package net.micode.notes.gtask.remote;
// 导入Android账号管理相关类
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; // 日志工具
// 导入AndroidX注解
import androidx.annotation.NonNull; // 非空参数注解
// 导入Google任务数据模型
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; // Google任务字符串常量工具
import net.micode.notes.ui.NotesPreferenceActivity; // 设置界面Activity
// 导入Apache HTTP组件已废弃但部分旧系统仍在使用
import org.apache.http.HttpEntity; // HTTP消息实体接口
import org.apache.http.HttpResponse; // HTTP响应对象
import org.apache.http.client.entity.UrlEncodedFormEntity; // URL编码表单实体
import org.apache.http.client.methods.HttpGet; // HTTP GET请求方法
import org.apache.http.client.methods.HttpPost; // HTTP POST请求方法
import org.apache.http.cookie.Cookie; // Cookie信息对象
import org.apache.http.impl.client.BasicCookieStore; // 基础Cookie存储
import org.apache.http.impl.client.DefaultHttpClient; // 默认HTTP客户端已废弃
import org.apache.http.message.BasicNameValuePair; // 键值对参数对象
import org.apache.http.params.BasicHttpParams; // HTTP参数集合
import org.apache.http.params.HttpConnectionParams; // 连接相关参数
import org.apache.http.params.HttpParams; // HTTP参数接口
import org.apache.http.params.HttpProtocolParams; // 协议相关参数
// 导入JSON处理类
import org.json.JSONArray; // JSON数组处理
import org.json.JSONException; // JSON解析异常
import org.json.JSONObject; // JSON对象处理
// 导入IO流相关类
import java.io.BufferedReader; // 缓冲字符输入流
import java.io.IOException; // IO异常
import java.io.InputStream; // 字节输入流
import java.io.InputStreamReader; // 字节流转字符流
import java.util.LinkedList; // 链表实现
import java.util.List; // 列表接口
// 导入压缩流处理
import java.util.zip.GZIPInputStream; // GZIP解压流
import java.util.zip.Inflater; // 解压器
import java.util.zip.InflaterInputStream; // 解压输入流
// Google任务客户端实现类负责与Google Tasks API交互
public class GTaskClient {
// 日志标签
private static final String TAG = GTaskClient.class.getSimpleName();
// Google任务基础URL
private static final String GTASK_URL = "https://mail.google.com/tasks/";
// GET请求URL
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
// POST请求URL
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// 单例实例
private static GTaskClient mInstance = null;
// HTTP客户端使用已废弃的Apache HTTP Client
private DefaultHttpClient mHttpClient;
// GET请求实际URL可能包含自定义域名
private String mGetUrl;
// POST请求实际URL可能包含自定义域名
private String mPostUrl;
// 客户端版本号
private long mClientVersion;
// 登录状态标识
private boolean mLoggedin;
// 上次登录时间戳
private long mLastLoginTime;
// 操作ID计数器
private int mActionId;
// 当前账号信息
private Account mAccount;
// 待提交的更新操作集合
private JSONArray mUpdateArray;
// 私有构造方法(单例模式)
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL; // 默认使用官方GET URL
mPostUrl = GTASK_POST_URL; // 默认使用官方POST URL
mClientVersion = -1; // 初始无效版本
mLoggedin = false; // 初始未登录
mLastLoginTime = 0; // 初始时间戳
mActionId = 1; // 操作ID从1开始
mAccount = null; // 初始无账号
mUpdateArray = null; // 初始无待更新操作
}
// 获取单例实例(线程安全)
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient(); // 懒加载初始化
}
return mInstance;
}
// 登录方法
public boolean login(Activity activity) {
// 检查登录是否过期假设cookie 5分钟后失效
final long interval = 1000 * 60 * 5; // 5分钟毫秒数
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false; // 标记需要重新登录
}
// 检查账号是否变更
if (mLoggedin && !TextUtils.equals(getSyncAccount().name,
NotesPreferenceActivity.getSyncAccountName(activity))) {
mLoggedin = false; // 账号变更需重新登录
}
// 已登录直接返回
if (mLoggedin) {
Log.d(TAG, "已经登录");
return true;
}
mLastLoginTime = System.currentTimeMillis(); // 更新登录时间
String authToken = loginGoogleAccount(activity, false); // 获取认证令牌
if (authToken == null) {
Log.e(TAG, "Google账号登录失败");
return false;
}
// 处理非gmail.com 域名的企业账号
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") ||
mAccount.name.toLowerCase().endsWith("googlemail.com"))) {
// 构建自定义域名URL
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix).append("/");
mGetUrl = url + "ig"; // 更新GET URL
mPostUrl = url + "r/ig"; // 更新POST URL
// 尝试用自定义域名登录
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// 自定义域名登录失败时尝试官方URL
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL; // 恢复默认GET URL
mPostUrl = GTASK_POST_URL; // 恢复默认POST URL
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
mLoggedin = true; // 标记登录成功
return true;
}
// 获取Google账号认证令牌
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
// 获取账号管理器实例
AccountManager accountManager = AccountManager.get(activity);
// 获取所有Google账号
Account[] accounts = accountManager.getAccountsByType("com.google");
if (accounts.length == 0) {
Log.e(TAG, "没有可用的Google账号");
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, "找不到设置中配置的账号");
return null;
}
// 异步获取认证令牌
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, "获取认证令牌失败");
authToken = null;
}
return authToken;
}
// 尝试登录Google Tasks服务
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (loginGtask(authToken)) {
// 令牌可能过期,失效后重试
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
Log.e(TAG, "Google账号登录失败");
return false;
}
if (loginGtask(authToken)) {
Log.e(TAG, "Google Tasks登录失败");
return false;
}
}
return true;
}
// 实际执行Google Tasks登录
private boolean loginGtask(String authToken) {
// 设置HTTP请求超时连接10秒socket15秒
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
// 初始化HTTP客户端
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore); // 设置cookie存储
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// 执行登录请求
try {
String loginUrl = mGetUrl + "?auth=" + authToken; // 构造登录URL
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = mHttpClient.execute(httpGet);
// 检查认证cookie
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, "缺少认证cookie");
}
// 从响应中提取客户端版本号
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 true; // 解析失败返回true触发重试
} catch (Exception e) {
Log.e(TAG, "HTTP GET请求失败");
return true; // 网络异常返回true触发重试
}
return false; // 登录成功返回false
}
// 生成递增的操作ID
private int getActionId() {
return mActionId++;
}
// 创建HTTP POST请求对象
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实体获取响应内容支持gzip/deflate压缩
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "内容编码: " + 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请求并返回JSON响应
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "请先登录");
throw new ActionFailureException("未登录");
}
HttpPost httpPost = createHttpPost();
try {
// 构造请求参数
LinkedList<BasicNameValuePair> list = new LinkedList<>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// 执行请求并解析响应
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("POST请求失败");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("响应内容转JSON失败");
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("请求过程中发生错误");
}
}
// 创建任务
public void createTask(Task task) throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建动作
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送请求并处理响应
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("创建任务时处理JSON失败");
}
}
// 创建任务列表
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 添加创建动作
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送请求并处理响应
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("创建任务列表时处理JSON失败");
}
}
// 提交所有待更新操作
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) { // 有待更新操作时才执行
try {
JSONObject jsPost = new JSONObject();
// 添加动作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// 添加客户端版本
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("提交更新时处理JSON失败");
}
}
}
// 添加节点更新操作
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// 避免过多更新操作限制最多10个
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate(); // 超过限制先提交
}
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
mUpdateArray.put(node.getUpdateAction(getActionId())); // 添加更新动作
}
}
// 移动任务
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 设置移动动作参数
action.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());
// 同列表移动时设置前驱任务ID
if (preParent == curParent && task.getPriorSibling() != null) {
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());
// 跨列表移动时设置目标列表ID
if (preParent != curParent) {
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost); // 发送请求
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("移动任务时处理JSON失败");
}
}
// 删除节点
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// 设置删除标记并添加更新动作
node.setDeleted(true);
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
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("删除节点时处理JSON失败");
}
}
// 获取所有任务列表
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "请先登录");
throw new ActionFailureException("未登录");
}
try {
HttpGet httpGet = new HttpGet(mGetUrl); // 创建GET请求
HttpResponse response = mHttpClient.execute(httpGet);
// 从响应中提取任务列表数据
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 (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("获取任务列表时HTTP请求失败");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("获取任务列表时处理JSON失败");
}
}
// 获取特定任务列表详情
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate(); // 先提交待更新操作
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = getJsonArray(listGid); // 获取动作数组
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 添加客户端版本
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("获取任务列表详情时处理JSON失败");
}
}
// 构造获取任务列表的动作数组
@NonNull
private JSONArray getJsonArray(String listGid) throws JSONException {
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// 设置获取全部动作参数
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);
return actionList;
}
// 获取当前同步账号
public Account getSyncAccount() {
return mAccount;
}
// 重置待更新操作数组
public void resetUpdateArray() {
mUpdateArray = null;
}
}

@ -0,0 +1,843 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义Google任务远程操作包路径
package net.micode.notes.gtask.remote;
// 导入Android基础组件
import android.app.Activity; // 活动组件,提供用户交互界面
import android.content.ContentResolver; // 内容解析器用于访问ContentProvider
import android.content.ContentUris; // 用于操作ContentProvider的URI工具
import android.content.ContentValues; // 键值对集合,用于数据库操作
import android.content.Context; // 应用上下文,提供全局资源访问
import android.database.Cursor; // 数据库查询结果集
import android.os.Build; // 系统版本信息
import android.util.Log; // 日志工具类
// 导入AndroidX兼容库注解
import androidx.annotation.RequiresApi; // API版本要求注解
// 导入应用资源
import net.micode.notes.R; // 资源ID引用
// 导入笔记数据相关类
import net.micode.notes.data.Notes; // 笔记数据库常量
import net.micode.notes.data.Notes.DataColumns; // 数据表列名常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记表列名常量
// 导入Google任务数据模型
import net.micode.notes.gtask.data.MetaData; // 元数据封装类
import net.micode.notes.gtask.data.Node; // 节点基类
import net.micode.notes.gtask.data.SqlNote; // SQLite笔记实体
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; // Google任务字符串工具
// 导入JSON处理库
import org.json.JSONArray; // JSON数组处理
import org.json.JSONException; // JSON解析异常
import org.json.JSONObject; // JSON对象处理
// 导入Java集合框架
import java.util.HashMap; // 哈希映射表
import java.util.HashSet; // 哈希集合
// 集合迭代器
import java.util.Map; // 映射接口
import java.util.Objects; // 对象工具类
// Google任务同步管理器实现本地笔记与Google Tasks双向同步
public class GTaskManager {
// 日志标签使用类名作为TAG
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; // 用于获取认证令牌的Activity
private Context mContext; // 应用上下文
private ContentResolver mContentResolver; // 内容解析器
// 同步状态标志
private boolean mSyncing; // 是否正在同步的标志
private boolean mCancelled; // 是否已取消同步的标志
// 数据存储结构
private final HashMap<String, TaskList> mGTaskListHashMap; // Google任务列表缓存key: 任务列表ID
private final HashMap<String, Node> mGTaskHashMap; // 所有任务节点缓存key: 任务ID
private final HashMap<String, MetaData> mMetaHashMap; // 元数据缓存key: 关联任务ID
private TaskList mMetaList; // 元数据专用任务列表
// ID映射关系
private final HashSet<Long> mLocalDeleteIdMap; // 待删除的本地笔记ID集合
private final HashMap<String, Long> mGidToNid; // Google任务ID到本地笔记ID的映射
private final HashMap<Long, String> mNidToGid; // 本地笔记ID到Google任务ID的映射
// 私有构造函数(单例模式)
private GTaskManager() {
mSyncing = false; // 初始化时未在同步状态
mCancelled = false; // 初始化时未取消
mGTaskListHashMap = new HashMap<>(); // 初始化任务列表缓存
mGTaskHashMap = new HashMap<>(); // 初始化任务节点缓存
mMetaHashMap = new HashMap<>(); // 初始化元数据缓存
mMetaList = null; // 初始化元数据列表为空
mLocalDeleteIdMap = new HashSet<>(); // 初始化待删除ID集合
mGidToNid = new HashMap<>(); // 初始化GoogleID到本地ID映射
mNidToGid = new HashMap<>(); // 初始化本地ID到GoogleID映射
}
// 获取单例实例(线程安全)
public static synchronized GTaskManager getInstance() {
if (mInstance == null) { // 如果实例不存在
mInstance = new GTaskManager(); // 创建新实例
}
return mInstance; // 返回单例实例
}
// 设置Activity上下文用于认证
public synchronized void setActivityContext(Activity activity) {
mActivity = activity; // 保存Activity引用
}
// 执行同步操作(主入口方法)
@RequiresApi(api = Build.VERSION_CODES.R)
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 {
// 获取Google任务客户端实例
GTaskClient client = GTaskClient.getInstance();
client.resetUpdateArray(); // 重置更新队列
// 步骤1登录Google Tasks如果未取消
if (!mCancelled) {
if (!client.login(mActivity)) { // 尝试登录
throw new NetworkFailureException("login google task failed"); // 登录失败抛出异常
}
}
// 步骤2初始化远程任务列表
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list)); // 更新进度
initGTaskList(); // 初始化任务列表
// 步骤3执行内容同步
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(); // 清空待删除ID集合
mGidToNid.clear(); // 清空ID映射
mNidToGid.clear(); // 清空反向ID映射
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();
// 首先初始化元数据列表
mMetaList = null; // 重置元数据列表
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i); // 获取单个列表
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取列表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); // 设置内容
// 加载该列表下的所有元数据项
JSONArray jsMetas = client.getTaskList(gid);
for (int j = 0; j < jsMetas.length(); j++) {
object = jsMetas.getJSONObject(j); // 获取元数据项
MetaData metaData = new MetaData(); // 创建元数据对象
metaData.setContentByRemoteJSON(object); // 设置内容
if (metaData.isWorthSaving()) { // 检查是否需要保存
mMetaList.addChildTask(metaData); // 添加到元数据列表
if (metaData.getGid() != null) { // 如果有有效ID
mMetaHashMap.put(metaData.getRelatedGid(), metaData); // 加入缓存
}
}
}
}
}
// 如果不存在元数据列表则创建
if (mMetaList == null) {
mMetaList = new TaskList(); // 创建新列表
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META); // 设置名称
GTaskClient.getInstance().createTaskList(mMetaList); // 在服务器创建
}
// 初始化普通任务列表
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i); // 获取单个列表
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取列表ID
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME); // 获取列表名称
// 检查是否是MIUI前缀的普通列表排除元数据列表
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); // 加入节点缓存
// 加载该列表下的所有任务
JSONArray jsTasks = client.getTaskList(gid);
for (int j = 0; j < jsTasks.length(); j++) {
object = jsTasks.getJSONObject(j); // 获取单个任务
gid = object.getString(GTaskStringUtils.GTASK_JSON_ID); // 获取任务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()); // 记录JSON异常
e.printStackTrace(); // 打印堆栈轨迹
throw new ActionFailureException("initGTaskList: handing JSONObject failed"); // 抛出操作异常
}
}
// 执行内容同步(核心方法)
@RequiresApi(api = Build.VERSION_CODES.R)
private void syncContent() throws NetworkFailureException {
int syncType; // 同步类型标识
Cursor c = null; // 数据库游标
String gid; // Google任务ID
Node node; // 任务节点
mLocalDeleteIdMap.clear(); // 清空待删除集合
if (mCancelled) return; // 检查是否已取消
/* ===== 阶段1处理本地已删除的笔记 ===== */
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); // 获取Google ID
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)); // 记录待删除ID
}
} else {
Log.w(TAG, "failed to query trash folder"); // 记录查询失败
}
} finally {
if (c != null) {
c.close(); // 关闭游标
c = null;
}
}
/* ===== 阶段2同步文件夹 ===== */
syncFolder(); // 同步文件夹结构
/* ===== 阶段3处理数据库中的现有笔记 ===== */
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); // 获取Google ID
node = mGTaskHashMap.get(gid); // 查找对应节点
// 确定同步类型
if (node != null) { // 如果存在对应节点
mGTaskHashMap.remove(gid); // 从缓存移除
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN)); // 建立ID映射
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid); // 建立反向映射
syncType = node.getSyncAction(c); // 获取同步类型
} else {
// 根据是否存在Google ID判断是本地新增还是远程删除
syncType = c.getString(SqlNote.GTASK_ID_COLUMN).trim().isEmpty() ?
Node.SYNC_ACTION_ADD_REMOTE : 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(); // 关闭游标
}
}
/* ===== 阶段4处理剩余的远程节点本地不存在 ===== */
for (Map.Entry<String, Node> entry : mGTaskHashMap.entrySet()) {
node = entry.getValue(); // 获取节点
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); // 执行本地添加
}
/* ===== 阶段5批量删除本地标记项 ===== */
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
throw new ActionFailureException("failed to batch-delete local deleted notes"); // 删除失败抛出异常
}
}
/* ===== 阶段6刷新本地同步ID ===== */
if (!mCancelled) {
GTaskClient.getInstance().commitUpdate(); // 提交未完成的更新
refreshLocalSyncId(); // 刷新本地同步ID
}
}
// 同步文件夹方法需要Android R及以上版本
@RequiresApi(api = Build.VERSION_CODES.R)
private void syncFolder() throws NetworkFailureException {
Cursor c = null; // 数据库查询游标
String gid; // Google任务ID
Node node; // 任务节点对象
int syncType; // 同步类型标识
if (mCancelled) return; // 检查同步是否被取消
/* ===== 1. 处理根文件夹 ===== */
try {
// 查询根文件夹ID_ROOT_FOLDER
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); // 获取Google ID
node = mGTaskHashMap.get(gid); // 查找对应节点
if (node != null) { // 如果远程存在对应节点
mGTaskHashMap.remove(gid); // 从缓存移除
// 建立ID映射关系
mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
// 系统文件夹:仅在名称不匹配时更新远程
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, null, c); // 在远程创建
}
} else {
Log.w(TAG, "failed to query root folder"); // 记录查询失败
}
} finally {
if (c != null) c.close(); // 确保游标关闭
}
/* ===== 2. 处理通话记录文件夹 ===== */
try {
// 查询通话记录文件夹ID_CALL_RECORD_FOLDER
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(_id=?)", new String[]{String.valueOf(Notes.ID_CALL_RECORD_FOLDER)}, null);
if (c != null && c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN); // 获取Google ID
node = mGTaskHashMap.get(gid); // 查找对应节点
if (node != null) { // 如果远程存在对应节点
mGTaskHashMap.remove(gid); // 从缓存移除
// 建立ID映射关系
mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
// 系统文件夹:仅在名称不匹配时更新远程
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, null, c); // 在远程创建
}
} else {
Log.w(TAG, "failed to query call note folder"); // 记录查询失败
}
} finally {
if (c != null) c.close(); // 确保游标关闭
}
/* ===== 3. 处理普通本地文件夹 ===== */
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); // 获取Google ID
node = mGTaskHashMap.get(gid); // 查找对应节点
// 确定同步类型
if (node != null) { // 存在对应节点
mGTaskHashMap.remove(gid); // 从缓存移除
// 建立ID映射关系
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c); // 获取同步类型
} else {
// 根据Google ID是否存在判断是本地新增还是远程删除
syncType = c.getString(SqlNote.GTASK_ID_COLUMN).trim().isEmpty() ?
Node.SYNC_ACTION_ADD_REMOTE : Node.SYNC_ACTION_DEL_LOCAL;
}
doContentSync(syncType, node, c); // 执行同步操作
}
} else {
Log.w(TAG, "failed to query existing folder"); // 记录查询失败
}
} finally {
if (c != null) c.close(); // 确保游标关闭
}
/* ===== 4. 处理远程新增的文件夹 ===== */
for (Map.Entry<String, TaskList> entry : mGTaskListHashMap.entrySet()) {
gid = entry.getKey(); // 获取Google ID
node = entry.getValue(); // 获取节点对象
// 如果缓存中仍存在该节点(说明本地不存在)
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid); // 从缓存移除
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null); // 在本地创建
}
}
// 如果未被取消,提交所有未完成的更新
if (!mCancelled) GTaskClient.getInstance().commitUpdate();
}
// 执行具体同步操作(根据同步类型分发)
@RequiresApi(api = Build.VERSION_CODES.R)
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)); // 记录待删除ID
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: // 冲突处理(目前简单采用远程更新)
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_NONE: // 无操作
break;
case Node.SYNC_ACTION_ERROR: // 错误状态
default:
throw new ActionFailureException("unkown sync action type"); // 抛出异常
}
}
// 在本地添加节点
@RequiresApi(api = Build.VERSION_CODES.R)
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); // 设置父ID为根文件夹
}
}
else { // 普通任务节点
sqlNote = new SqlNote(mContext);
JSONObject js = node.getLocalJSONFromContent(); // 获取节点内容
try {
// 处理笔记ID冲突
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)) {
note.remove(NoteColumns.ID); // ID已存在则移除
}
}
}
// 处理数据项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)) {
data.remove(DataColumns.ID); // ID已存在则移除
}
}
}
}
} catch (JSONException e) {
Log.w(TAG, e.toString()); // 记录JSON异常
e.printStackTrace();
}
sqlNote.setContent(js); // 设置笔记内容
// 设置父文件夹ID
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); // 设置父节点ID
}
// 创建本地节点
sqlNote.setGtaskId(node.getGid()); // 设置Google任务ID
sqlNote.commit(false); // 提交到数据库
// 更新ID映射关系
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
// 更新远程元数据
updateRemoteMeta(node.getGid(), sqlNote);
}
// 更新本地节点需要Android R及以上版本
@RequiresApi(api = Build.VERSION_CODES.R)
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return; // 如果同步已取消则直接返回
}
SqlNote sqlNote;
// 步骤1更新本地笔记
sqlNote = new SqlNote(mContext, c); // 根据游标创建SqlNote对象
sqlNote.setContent(node.getLocalJSONFromContent()); // 设置节点内容
// 步骤2设置父节点ID任务节点取父任务列表ID否则使用根文件夹ID
Long parentId = (node instanceof Task) ?
mGidToNid.get(((Task) node).getParent().getGid()) : // 获取任务的父列表本地ID
Long.valueOf(Notes.ID_ROOT_FOLDER); // 默认使用根文件夹ID
if (parentId == null) {
Log.e(TAG, "无法在本地找到任务的父ID");
throw new ActionFailureException("无法更新本地节点");
}
sqlNote.setParentId(parentId); // 设置父节点ID
sqlNote.commit(true); // 提交修改到数据库
// 步骤3更新元数据信息
updateRemoteMeta(node.getGid(), sqlNote);
}
// 添加远程节点需要Android R及以上版本
@RequiresApi(api = Build.VERSION_CODES.R)
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return; // 如果同步已取消则直接返回
}
SqlNote sqlNote = new SqlNote(mContext, c); // 根据游标创建SqlNote对象
Node n; // 要创建的节点
// 步骤1判断节点类型并处理
if (sqlNote.isNoteType()) {
// 情况1普通笔记节点
Task task = new Task(); // 创建新任务
task.setContentByLocalJSON(sqlNote.getContent()); // 设置任务内容
// 获取父任务列表的Google ID
String parentGid = mNidToGid.get(sqlNote.getParentId());
if (parentGid == null) {
Log.e(TAG, "无法找到任务的父任务列表");
throw new ActionFailureException("无法添加远程任务");
}
// 将任务添加到父列表
Objects.requireNonNull(mGTaskListHashMap.get(parentGid)).addChildTask(task);
// 在远程创建任务
GTaskClient.getInstance().createTask(task);
n = task; // 设置返回节点
// 添加元数据
updateRemoteMeta(task.getGid(), sqlNote);
} else {
// 情况2文件夹节点
TaskList tasklist = null;
// 构建文件夹名称带MIUI前缀
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(); // 普通文件夹使用笔记摘要
}
// 检查是否已存在同名文件夹
for (Map.Entry<String, TaskList> entry : mGTaskListHashMap.entrySet()) {
String gid = entry.getKey();
TaskList list = entry.getValue();
if (list.getName().equals(folderName)) {
tasklist = list; // 找到已有文件夹
mGTaskHashMap.remove(gid); // 从缓存移除
break;
}
}
// 如果不存在则创建新文件夹
if (tasklist == null) {
tasklist = new TaskList(); // 创建新任务列表
tasklist.setContentByLocalJSON(sqlNote.getContent()); // 设置内容
GTaskClient.getInstance().createTaskList(tasklist); // 远程创建
mGTaskListHashMap.put(tasklist.getGid(), tasklist); // 加入缓存
}
n = tasklist; // 设置返回节点
}
// 步骤2更新本地笔记
sqlNote.setGtaskId(n.getGid()); // 设置Google任务ID
sqlNote.commit(false); // 提交修改到数据库
sqlNote.resetLocalModified(); // 重置本地修改标记
sqlNote.commit(true); // 再次提交确保状态更新
// 步骤3更新ID映射关系
mGidToNid.put(n.getGid(), sqlNote.getId()); // Google ID → 本地ID
mNidToGid.put(sqlNote.getId(), n.getGid()); // 本地ID → Google ID
}
// 更新远程节点需要Android R及以上版本
@RequiresApi(api = Build.VERSION_CODES.R)
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return; // 如果同步已取消则直接返回
}
SqlNote sqlNote = new SqlNote(mContext, c); // 根据游标创建SqlNote对象
// 步骤1更新远程节点内容
node.setContentByLocalJSON(sqlNote.getContent()); // 设置节点内容
GTaskClient.getInstance().addUpdateNode(node); // 添加更新到队列
// 步骤2更新元数据
updateRemoteMeta(node.getGid(), sqlNote);
// 步骤3处理任务移动如果是笔记类型
if (sqlNote.isNoteType()) {
Task task = (Task) node; // 转换为任务对象
TaskList preParentList = task.getParent(); // 获取原父列表
// 获取当前父列表的Google ID
String curParentGid = mNidToGid.get(sqlNote.getParentId());
if (curParentGid == null) {
Log.e(TAG, "无法找到任务的父任务列表");
throw new ActionFailureException("无法更新远程任务");
}
TaskList curParentList = mGTaskListHashMap.get(curParentGid); // 获取当前父列表
// 如果父列表发生变化
if (preParentList != curParentList) {
preParentList.removeChildTask(task); // 从原列表移除
if (curParentList != null) {
curParentList.addChildTask(task); // 添加到新列表
}
// 在远程执行移动操作
GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
}
}
// 步骤4清除本地修改标记
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) {
// 情况1元数据已存在 - 更新操作
metaData.setMeta(gid, sqlNote.getContent()); // 设置新的元数据内容
GTaskClient.getInstance().addUpdateNode(metaData); // 添加到更新队列
} else {
// 情况2元数据不存在 - 创建操作
metaData = new MetaData(); // 创建新元数据对象
metaData.setMeta(gid, sqlNote.getContent()); // 设置元数据内容
mMetaList.addChildTask(metaData); // 添加到元数据列表
mMetaHashMap.put(gid, metaData); // 加入缓存
GTaskClient.getInstance().createTask(metaData); // 在远程创建
}
}
}
// 刷新本地同步ID确保与远程一致
private void refreshLocalSyncId() throws NetworkFailureException {
if (mCancelled) {
return; // 如果同步已取消则直接返回
}
/* 步骤1重新加载远程任务列表 */
mGTaskHashMap.clear(); // 清空任务缓存
mGTaskListHashMap.clear(); // 清空任务列表缓存
mMetaHashMap.clear(); // 清空元数据缓存
initGTaskList(); // 重新初始化任务列表
/* 步骤2更新本地同步ID */
try (Cursor 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); // 获取Google ID
Node node = mGTaskHashMap.get(gid); // 查找对应远程节点
if (node != null) {
mGTaskHashMap.remove(gid); // 从缓存移除
// 准备更新数据
ContentValues values = new ContentValues();
// 使用远程节点的最后修改时间作为同步ID
values.put(NoteColumns.SYNC_ID, node.getLastModified());
// 更新数据库
mContentResolver.update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(SqlNote.ID_COLUMN)), // 定位要更新的笔记
values, // 更新内容
null, // WHERE条件
null // WHERE参数
);
} else {
// 错误处理:本地存在但远程不存在的笔记
Log.e(TAG, "本地存在但远程找不到对应的任务");
throw new ActionFailureException("同步后仍有本地条目缺少Google ID");
}
}
} else {
Log.w(TAG, "查询本地笔记刷新同步ID失败");
}
}
// 确保关闭游标
}
// 获取当前同步账号名称
public String getSyncAccount() {
// 从GTaskClient获取账号信息并返回名称
return GTaskClient.getInstance().getSyncAccount().name;
}
// 取消同步操作
public void cancelSync() {
mCancelled = true; // 设置取消标志
}
}

@ -0,0 +1,167 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明Google任务同步服务类继承自Service
package net.micode.notes.gtask.remote;
// 导入安卓活动(Activity)相关类
// 提供用户交互界面的基础组件,代表一个应用屏幕
import android.app.Activity;
// 导入安卓服务(Service)相关类
// 后台运行组件,无用户界面,用于执行长时间操作
import android.app.Service;
// 导入上下文(Context)相关类
// 提供应用环境全局信息,是访问应用资源的入口
import android.content.Context;
// 导入意图(Intent)相关类
// 用于组件间通信的消息对象,可包含操作指令和数据
import android.content.Intent;
// 导入Bundle数据包相关类
// 键值对集合,用于在不同组件间传递数据
import android.os.Bundle;
// 导入绑定接口(IBinder)相关类
// 跨进程通信(IPC)的核心接口用于Service与Activity绑定
import android.os.IBinder;
// Google任务同步后台服务
public class GTaskSyncService extends Service {
// 同步动作的Intent键名
public final static String ACTION_STRING_NAME = "sync_action_type";
// 启动同步的动作值
public final static int ACTION_START_SYNC = 0;
// 取消同步的动作值
public final static int ACTION_CANCEL_SYNC = 1;
// 无效动作值
public final static int ACTION_INVALID = 2;
// 广播名称
public final static String GTASK_SERVICE_BROADCAST_NAME =
"net.micode.notes.gtask.remote.gtask_sync_service";
// 广播键名:是否正在同步
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
// 广播键名:进度消息
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
// 同步任务实例
private static GTaskASyncTask mSyncTask = null;
// 同步进度文本
private static String mSyncProgress = "";
// 开始同步方法
private void startSync() {
if (mSyncTask == null) { // 如果没有正在进行的同步任务
// 创建新的异步任务实例
mSyncTask = new GTaskASyncTask(this, () -> {
// 同步完成后的回调
mSyncTask = null; // 清空任务引用
sendBroadcast(""); // 发送空广播
stopSelf(); // 停止服务
});
sendBroadcast(""); // 发送初始广播
mSyncTask.execute(); // 执行异步任务
}
}
// 取消同步方法
private void cancelSync() {
if (mSyncTask != null) { // 如果存在同步任务
mSyncTask.cancelSync(); // 调用取消方法
}
}
// 服务创建时调用
@Override
public void onCreate() {
mSyncTask = null; // 初始化任务引用
}
// 服务启动命令处理
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras(); // 获取附加数据
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
// 根据动作类型执行不同操作
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
startSync(); // 开始同步
break;
case ACTION_CANCEL_SYNC:
cancelSync(); // 取消同步
break;
default:
break;
}
return START_STICKY; // 服务被终止后自动重启
}
return super.onStartCommand(intent, flags, startId);
}
// 内存不足时调用
@Override
public void onLowMemory() {
if (mSyncTask != null) { // 如果正在同步
mSyncTask.cancelSync(); // 取消当前同步
}
}
// 绑定服务时调用(本服务不需要绑定)
@Override
public IBinder onBind(Intent intent) {
return null; // 返回null表示不支持绑定
}
// 发送广播方法
public void sendBroadcast(String msg) {
mSyncProgress = msg; // 更新进度文本
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME); // 创建广播Intent
// 添加是否正在同步的状态
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
// 添加进度消息
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
sendBroadcast(intent); // 发送广播
}
// 静态方法从Activity启动同步
public static void startSync(Activity activity) {
GTaskManager.getInstance().setActivityContext(activity); // 设置活动上下文
// 创建启动服务的Intent
Intent intent = new Intent(activity, GTaskSyncService.class);
// 设置动作为开始同步
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME,
GTaskSyncService.ACTION_START_SYNC);
activity.startService(intent); // 启动服务
}
// 静态方法:取消同步
public static void cancelSync(Context context) {
// 创建取消同步的服务Intent
Intent intent = new Intent(context, GTaskSyncService.class);
// 设置动作为取消同步
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME,
GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent); // 启动服务
}
// 静态方法:检查是否正在同步
public static boolean isSyncing() {
return mSyncTask != null; // 通过任务实例判断
}
// 静态方法:获取当前进度文本
public static String getProgressString() {
return mSyncProgress; // 返回进度消息
}
}

@ -0,0 +1,342 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义笔记模型的包路径
package net.micode.notes.model;
// 导入所需的Android类库
import android.content.ContentProviderOperation; // 内容提供者批量操作
import android.content.ContentProviderResult; // 内容提供者操作结果
import android.content.ContentUris; // URI工具类
import android.content.ContentValues; // 键值对存储类
import android.content.Context; // 上下文对象
import android.content.OperationApplicationException;// 操作应用异常
import android.net.Uri; // URI处理类
import android.os.Build;
import android.os.RemoteException; // 远程调用异常
import android.util.Log; // 日志工具
// 导入项目相关的数据类
import androidx.annotation.RequiresApi;
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; // 文本笔记类型
// 导入Java工具类
import java.util.ArrayList; // 动态数组
import java.util.Objects;
// 笔记模型类
public class Note {
private final ContentValues mNoteDiffValues; // 存储笔记差异值的ContentValues
private final NoteData mNoteData; // 笔记数据对象
private static final String TAG = "Note"; // 日志标签
/**
* ID
* @param context
* @param folderId ID
* @return ID
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// 创建ContentValues存储新笔记初始值
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); // 设置所属文件夹ID
// 通过内容解析器插入新笔记
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
try {
// 从返回的URI中解析出笔记ID
noteId = Long.parseLong(Objects.requireNonNull(uri).getPathSegments().get(1));
} catch (NumberFormatException e) {
// 处理数字格式异常
Log.e(TAG, "Get note id error :" + e);
}
// 检查笔记ID是否合法
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
// 构造函数
public Note() {
mNoteDiffValues = new ContentValues(); // 初始化笔记差异值存储
mNoteData = new NoteData(); // 初始化笔记数据对象
}
/**
*
* @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()); // 更新修改时间
}
/**
*
* @param key
* @param value
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value); // 委托给NoteData对象处理
}
/**
* ID
* @param id ID
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id); // 委托给NoteData对象处理
}
/**
* ID
* @return ID
*/
public long getTextDataId() {
return mNoteData.mTextDataId; // 从NoteData对象获取
}
/**
* ID
* @param id ID
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id); // 委托给NoteData对象处理
}
/**
*
* @param key
* @param value
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value); // 委托给NoteData对象处理
}
/**
*
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean isLocalModified() {
return !mNoteDiffValues.isEmpty() || mNoteData.isLocalModified();
}
/**
*
* @param context
* @param noteId ID
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.R)
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 如果没有修改则直接返回成功
if (!isLocalModified()) {
return true;
}
/*
* LOCAL_MODIFIEDMODIFIED_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(); // 清空已应用的差异值
// 如果有笔记数据修改且推送失败,返回失败
return !mNoteData.isLocalModified()
|| (mNoteData.pushIntoContentResolver(context, noteId) != null);
}
// 内部类:笔记数据
private class NoteData {
private long mTextDataId; // 文本数据ID
private final ContentValues mTextDataValues; // 文本数据差异值
private long mCallDataId; // 通话数据ID
private final ContentValues mCallDataValues; // 通话数据差异值
private static final String TAG = "NoteData"; // 日志标签
// 构造函数
public NoteData() {
mTextDataValues = new ContentValues(); // 初始化文本数据存储
mCallDataValues = new ContentValues(); // 初始化通话数据存储
mTextDataId = 0; // 初始化文本数据ID
mCallDataId = 0; // 初始化通话数据ID
}
/**
*
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.R)
boolean isLocalModified() {
return !mTextDataValues.isEmpty() || !mCallDataValues.isEmpty();
}
/**
* ID
* @param id ID
*/
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
/**
* ID
* @param id ID
*/
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
/**
*
* @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()); // 更新修改时间
}
/**
*
* @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()); // 更新修改时间
}
/**
*
* @param context
* @param noteId ID
* @return URI
*/
@RequiresApi(api = Build.VERSION_CODES.R)
Uri pushIntoContentResolver(Context context, long noteId) {
// 安全检查
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 创建批量操作列表
ArrayList<ContentProviderOperation> operationList = new ArrayList<>();
ContentProviderOperation.Builder builder;
// 处理文本数据
if(!mTextDataValues.isEmpty()) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 设置所属笔记ID
// 如果是新数据则插入
if (mTextDataId == 0) {
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
// 从返回的URI中获取新数据ID
if (uri != null) {
setTextDataId(Long.parseLong(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.isEmpty()) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 设置所属笔记ID
// 如果是新数据则插入
if (mCallDataId == 0) {
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
// 从返回的URI中获取新数据ID
setCallDataId(Long.parseLong(Objects.requireNonNull(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.isEmpty()) {
try {
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
// 返回操作结果
return results.length == 0 || results[0] == null ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e, e.getMessage()));
return null;
}
}
return null;
}
}
}

@ -0,0 +1,441 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义工作笔记模型的包路径
package net.micode.notes.model;
// 导入所需的Android类库
import android.appwidget.AppWidgetManager; // 应用小部件管理
import android.content.ContentUris; // URI工具类
import android.content.Context; // 上下文对象
import android.database.Cursor; // 数据库查询结果
import android.os.Build;
import android.text.TextUtils; // 文本处理工具
import android.util.Log; // 日志工具
// 导入项目相关的数据类
import androidx.annotation.RequiresApi;
import net.micode.notes.data.Notes; // 笔记常量定义
import net.micode.notes.data.Notes.CallNote; // 通话笔记类型
import net.micode.notes.data.Notes.DataColumns; // 数据列定义
import net.micode.notes.data.Notes.DataConstants; // 数据常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记列定义
import net.micode.notes.data.Notes.TextNote; // 文本笔记类型
import net.micode.notes.tool.ResourceParser.NoteBgResources; // 笔记背景资源
// 工作笔记类,封装当前编辑笔记的所有操作
public class WorkingNote {
// 成员变量定义
private final Note mNote; // 基础笔记对象
private long mNoteId; // 笔记ID
public String mContent; // 笔记内容
private int mMode; /mMode/ /
private long mAlertDate; // 提醒时间
private long mModifiedDate; // 最后修改时间
private int mBgColorId; // 背景颜色ID
private int mWidgetId; // 关联小部件ID
private int mWidgetType; // 小部件类型
private long mFolderId; // 所在文件夹ID
private final Context mContext; // 上下文对象
private static final String TAG = "WorkingNote"; // 日志标签
private boolean mIsDeleted; // 是否已删除标记
private NoteSettingChangedListener mNoteSettingStatusListener; // 笔记设置变更监听器
// 数据表查询列定义
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID, // 数据ID
DataColumns.CONTENT, // 内容
DataColumns.MIME_TYPE, // MIME类型
DataColumns.DATA1, // 扩展数据1
DataColumns.DATA2, // 扩展数据2
DataColumns.DATA3, // 扩展数据3
DataColumns.DATA4, // 扩展数据4
};
// 笔记表查询列定义
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID, // 父文件夹ID
NoteColumns.ALERTED_DATE, // 提醒日期
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
NoteColumns.MODIFIED_DATE // 修改日期
};
// 数据表列索引常量
private static final int DATA_ID_COLUMN = 0; // ID列索引
private static final int DATA_CONTENT_COLUMN = 1; // 内容列索引
private static final int DATA_MIME_TYPE_COLUMN = 2; // MIME类型列索引
private static final int DATA_MODE_COLUMN = 3; // 模式列索引
// 笔记表列索引常量
private static final int NOTE_PARENT_ID_COLUMN = 0; // 父文件夹ID列索引
private static final int NOTE_ALERTED_DATE_COLUMN = 1; // 提醒日期列索引
private static final int NOTE_BG_COLOR_ID_COLUMN = 2; // 背景颜色ID列索引
private static final int NOTE_WIDGET_ID_COLUMN = 3; // 小部件ID列索引
private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 小部件类型列索引
private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改日期列索引
/**
*
* @param context
* @param folderId ID
*/
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0; // 初始无提醒
mModifiedDate = System.currentTimeMillis(); // 设置当前时间为修改时间
mFolderId = folderId; // 设置所属文件夹
mNote = new Note(); // 创建基础笔记对象
mNoteId = 0; // 新笔记ID为0
mIsDeleted = false; // 未删除状态
mMode = 0; // 默认普通模式
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小部件类型
}
/**
*
* @param context
* @param noteId ID
* @param folderId ID
*/
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId; // 设置笔记ID
mFolderId = folderId; // 设置所属文件夹
mIsDeleted = false; // 未删除状态
mNote = new Note(); // 创建基础笔记对象
loadNote(); // 加载笔记数据
}
/**
*
*/
private void loadNote() {
// 查询笔记表数据
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
NOTE_PROJECTION, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
// 从游标中读取笔记属性
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
}
cursor.close(); // 关闭游标
} else {
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData(); // 加载笔记内容数据
}
/**
*
*/
private void loadNoteData() {
// 查询数据表内容
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] { String.valueOf(mNoteId) }, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) { // 文本笔记数据
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) { // 通话笔记数据
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext());
}
cursor.close(); // 关闭游标
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
/**
*
* @param context
* @param folderId ID
* @param widgetId ID
* @param widgetType
* @param defaultBgColorId ID
* @return
*/
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); // 设置小部件ID
note.setWidgetType(widgetType); // 设置小部件类型
return note;
}
/**
* ID
* @param context
* @param id ID
* @return
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
*
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.R)
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);
// 如果有关联小部件,通知更新
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return true;
} else {
return false;
}
}
/**
*
* @return
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
*
* @return
*/
@RequiresApi(api = Build.VERSION_CODES.R)
private boolean isWorthSaving() {
// 已有笔记但未修改
return !mIsDeleted && // 已删除
(existInDatabase() || !TextUtils.isEmpty(mContent)) && // 新笔记且内容为空
(!existInDatabase() || mNote.isLocalModified());
}
/**
*
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
*
* @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);
}
}
/**
*
* @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
* @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));
}
}
/**
*
* @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));
}
}
/**
*
* @param type
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
/**
* ID
* @param id ID
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
/**
*
* @param text
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
/**
*
* @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));
}
// 以下是一系列获取笔记属性的方法
public boolean hasClockAlert() {
return (mAlertDate > 0);
}
public String getContent() {
return mContent;
}
public long getAlertDate() {
return mAlertDate;
}
public long getModifiedDate() {
return mModifiedDate;
}
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
public int getBgColorId() {
return mBgColorId;
}
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
public int getCheckListMode() {
return mMode;
}
public long getNoteId() {
return mNoteId;
}
public long getFolderId() {
return mFolderId;
}
public int getWidgetId() {
return mWidgetId;
}
public int getWidgetType() {
return mWidgetType;
}
/**
*
*/
public interface NoteSettingChangedListener {
/**
*
*/
void onBackgroundColorChanged();
/**
*
* @param date
* @param set
*/
void onClockAlertChanged(long date, boolean set);
/**
*
*/
void onWidgetChanged();
/**
*
* @param oldMode
* @param newMode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
}

@ -0,0 +1,398 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义备份工具类的包路径
package net.micode.notes.tool;
// 导入所需的Android类库
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; // 笔记列定义
// 导入Java IO类
import java.io.File; // 文件操作类
import java.io.FileNotFoundException; // 文件未找到异常
import java.io.FileOutputStream; // 文件输出流
import java.io.IOException; // IO异常
import java.io.PrintStream; // 打印流
/**
*
*/
public class BackupUtils {
public static final int STATE_SD_CARD_UNMOUNTED =1;
private static final String TAG = "BackupUtils"; // 日志标签
// 单例实现部分
private static BackupUtils sInstance; // 单例实例
/**
*
* @param context
* @return
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
/**
* /
*/
// SD卡未挂载状态
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// 备份文件不存在状态
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// 数据格式损坏状态
public static final int STATE_DATA_DESTROIED = 2;
// 系统错误状态
public static final int STATE_SYSTEM_ERROR = 3;
// 操作成功状态
public static final int STATE_SUCCESS = 4;
private final TextExport mTextExport; // 文本导出器
/**
*
* @param context
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context); // 初始化文本导出器
}
/**
*
* @return
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
*
* @return
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
* @return
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
*/
private static class TextExport {
// 笔记表查询列定义
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID, // 笔记ID
NoteColumns.MODIFIED_DATE, // 修改日期
NoteColumns.SNIPPET, // 笔记摘要
NoteColumns.TYPE // 笔记类型
};
// 笔记表列索引常量
private static final int NOTE_COLUMN_ID = 0; // ID列索引
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, // MIME类型
DataColumns.DATA1, // 扩展数据1
DataColumns.DATA2, // 扩展数据2
DataColumns.DATA3, // 扩展数据3
DataColumns.DATA4, // 扩展数据4
};
// 数据表列索引常量
private static final int DATA_COLUMN_CONTENT = 0; // 内容列索引
private static final int DATA_COLUMN_MIME_TYPE = 1; // MIME类型列索引
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 final 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 ID
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
*
* @param folderId ID
* @param ps
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询属于该文件夹的所有笔记
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 {
// 打印笔记的修改日期
ps.printf((getFormat(FORMAT_NOTE_DATE)) + "%n", DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)));
// 导出该笔记的内容
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
notesCursor.close(); // 关闭游标
}
}
/**
*
* @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)) { // 通话笔记
// 打印电话号码
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.printf((getFormat(FORMAT_NOTE_CONTENT)) + "%n",
phoneNumber);
}
// 打印通话日期
ps.printf((getFormat(FORMAT_NOTE_CONTENT)) + "%n", DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate));
// 打印通话位置
if (!TextUtils.isEmpty(location)) {
ps.printf((getFormat(FORMAT_NOTE_CONTENT)) + "%n",
location);
}
} else if (DataConstants.NOTE.equals(mimeType)) { // 普通笔记
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
ps.printf((getFormat(FORMAT_NOTE_CONTENT)) + "%n",
content);
}
}
} while (dataCursor.moveToNext());
}
dataCursor.close(); // 关闭游标
}
// 在笔记之间打印分隔线
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
});
} catch (IOException e) {
Log.e(TAG, e.toString());
}
}
/**
*
* @return
*/
public int exportToText() {
// 检查SD卡是否挂载
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;
}
// 首先导出文件夹及其笔记
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 {
// 打印文件夹名称
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.printf((getFormat(FORMAT_FOLDER_NAME)) + "%n", folderName);
}
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps); // 导出文件夹内容
} while (folderCursor.moveToNext());
}
folderCursor.close(); // 关闭游标
}
// 导出根目录下的笔记
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.printf((getFormat(FORMAT_NOTE_DATE)) + "%n", DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)));
// 导出该笔记的内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close(); // 关闭游标
}
ps.close(); // 关闭打印流
return STATE_SUCCESS; // 返回成功状态
}
/**
*
* @return
*/
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;
try {
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException | NullPointerException e) {
e.printStackTrace();
return null;
}
return ps;
}
}
/**
* SD
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
// 构建文件路径
sb.append(Environment.getExternalStorageDirectory()); // SD卡根目录
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 | IOException e) {
e.printStackTrace();
}
return null; // 创建失败返回null
}
}

@ -0,0 +1,403 @@
/*
* (c) 2010-2011MiCode (www.mi code.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义数据工具类的包路径
package net.micode.notes.tool;
// 导入Android内容提供者相关类
import android.content.ContentProviderOperation; // 内容提供者操作
import android.content.ContentProviderResult; // 内容提供者操作结果
import android.content.ContentResolver; // 内容解析器
import android.content.ContentUris; // URI工具
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; // 哈希集合
import android.os.Environment;
/**
*
*/
public class DataUtils {
// 日志标签
public static final String TAG = "DataUtils";
/**
*
* @param resolver
* @param ids ID
* @return
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
// 参数校验
if (ids == null) {
Log.d(TAG, "the ids is null");
return true; // 没有要删除的笔记视为成功
}
if (ids.isEmpty()) {
Log.d(TAG, "no id is in the hashset");
return true; // 空集合视为成功
}
// 创建批量操作列表
ArrayList<ContentProviderOperation> operationList = new ArrayList<>();
// 遍历要删除的笔记ID
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.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids);
return false;
}
return true;
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e, e.getMessage()));
}
return false;
}
/**
*
* @param resolver
* @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); // 设置新父文件夹ID
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原父文件夹ID
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已修改
// 执行更新
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
* @param resolver
* @param ids ID
* @param folderId ID
* @return
*/
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<>();
// 遍历要移动的笔记ID
for (long id : ids) {
// 创建更新操作
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新父文件夹ID
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已修改
operationList.add(builder.build()); // 添加到操作列表
}
try {
// 执行批量操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 检查操作结果
if (results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids);
return false;
}
return true;
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e, e.getMessage()));
}
return false;
}
/**
*
* @param resolver
* @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);
} finally {
cursor.close(); // 确保关闭游标
}
}
}
return count;
}
/**
*
* @param resolver
* @param noteId ID
* @param type
* @return
*/
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;
}
/**
*
* @param resolver
* @param noteId ID
* @return
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 查询指定ID的笔记
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;
}
/**
*
* @param resolver
* @param dataId ID
* @return
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
// 查询指定ID的数据项
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;
}
/**
*
* @param resolver
* @param name
* @return
*/
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;
}
/**
*
* @param resolver
* @param folderId ID
* @return
*/
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 }, // 查询小部件ID和类型
NoteColumns.PARENT_ID + "=?", // 指定父文件夹
new String[] { String.valueOf(folderId) }, // 参数值
null); // 无排序
HashSet<AppWidgetAttribute> set = null;
if (c != null) {
if (c.moveToFirst()) {
set = new HashSet<>(); // 创建集合
do {
try {
// 创建小部件属性对象并添加到集合
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0); // 小部件ID
widget.widgetType.set(c.getInt(1)); // 小部件类型
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
}
} while (c.moveToNext());
}
c.close(); // 关闭游标
}
return set;
}
/**
* ID
* @param resolver
* @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 (cursor) {
return cursor.getString(0); // 返回电话号码
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e);
}
// 确保关闭游标
}
return ""; // 未找到返回空字符串
}
/**
* ID
* @param resolver
* @param phoneNumber
* @param callDate
* @return ID
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
// 查询符合条件的数据项
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID }, // 只查询笔记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); // 返回笔记ID
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e);
}
}
cursor.close(); // 关闭游标
}
return 0; // 未找到返回0
}
/**
* ID
* @param resolver
* @param noteId ID
* @return
*/
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); // 未找到抛出异常
}
/**
*
* @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;
}
// 在DataUtils类中添加方法
public static boolean externalStorageAvailable() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
}

@ -0,0 +1,119 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.tool;
/**
* Google Tasks API JSON
* Google Tasks API使JSON
*/
public class GTaskStringUtils {
// ======================== Google Tasks API JSON字段常量 ========================
// 操作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字段用于创建后获取服务器分配的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笔记特定常量 ========================
// MIUI笔记文件夹前缀用于标识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";
// ======================== 元数据相关常量 ========================
// 元数据头-Google Task 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,283 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.tool;
// 导入Android相关类
import android.content.Context; // 上下文对象
import android.preference.PreferenceManager; // 偏好设置管理器
// 导入项目资源
import net.micode.notes.R; // 项目资源文件
import net.micode.notes.ui.NotesPreferenceActivity; // 偏好设置Activity
/**
*
* UI
*/
public class ResourceParser {
// ======================== 颜色常量定义 ========================
// 黄色背景标识
public static final int YELLOW = 0;
// 蓝色背景标识
public static final int BLUE = 1;
// 白色背景标识
public static final int WHITE = 2;
// 绿色背景标识
public static final int GREEN = 3;
// 红色背景标识
public static final int RED = 4;
// 默认背景颜色(黄色)
public static final int BG_DEFAULT_COLOR = YELLOW;
// ======================== 字体大小常量定义 ========================
// 小号字体
public static final int TEXT_SMALL = 0;
// 中号字体
public static final int TEXT_MEDIUM = 1;
// 大号字体
public static final int TEXT_LARGE = 2;
// 超大号字体
public static final int TEXT_SUPER = 3;
// 默认字体大小(中号)
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
*
*
*/
public static class NoteBgResources {
// 编辑区域背景资源数组
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow, // 黄色背景
R.drawable.edit_blue, // 蓝色背景
R.drawable.edit_white, // 白色背景
R.drawable.edit_green, // 绿色背景
R.drawable.edit_red // 红色背景
};
// 编辑区域标题背景资源数组
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow, // 黄色标题背景
R.drawable.edit_title_blue, // 蓝色标题背景
R.drawable.edit_title_white, // 白色标题背景
R.drawable.edit_title_green, // 绿色标题背景
R.drawable.edit_title_red // 红色标题背景
};
/**
*
* @param id IDYELLOW/BLUE/WHITE/GREEN/RED
* @return ID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
*
* @param id IDYELLOW/BLUE/WHITE/GREEN/RED
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
/**
* ID
* @param context
* @return ID
*/
public static int getDefaultBgId(Context context) {
// 检查用户是否启用了随机背景色
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
// 返回随机背景色
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
// 返回默认背景色
return BG_DEFAULT_COLOR;
}
}
/**
*
*
*/
public static class NoteItemBgResources {
// 列表第一项背景资源数组
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up, // 黄色背景第一项
R.drawable.list_blue_up, // 蓝色背景第一项
R.drawable.list_white_up, // 白色背景第一项
R.drawable.list_green_up, // 绿色背景第一项
R.drawable.list_red_up // 红色背景第一项
};
// 列表中间项背景资源数组
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle, // 黄色背景中间项
R.drawable.list_blue_middle, // 蓝色背景中间项
R.drawable.list_white_middle, // 白色背景中间项
R.drawable.list_green_middle, // 绿色背景中间项
R.drawable.list_red_middle // 红色背景中间项
};
// 列表最后一项背景资源数组
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down, // 黄色背景最后一项
R.drawable.list_blue_down, // 蓝色背景最后一项
R.drawable.list_white_down, // 白色背景最后一项
R.drawable.list_green_down, // 绿色背景最后一项
R.drawable.list_red_down, // 红色背景最后一项
};
// 列表单项背景资源数组(当列表只有一项时使用)
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single, // 黄色背景单项
R.drawable.list_blue_single, // 蓝色背景单项
R.drawable.list_white_single, // 白色背景单项
R.drawable.list_green_single, // 绿色背景单项
R.drawable.list_red_single // 红色背景单项
};
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
*
* @return ID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
/**
*
*
*/
public static class WidgetBgResources {
// 2x大小小部件背景资源数组
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow, // 黄色2x小部件
R.drawable.widget_2x_blue, // 蓝色2x小部件
R.drawable.widget_2x_white, // 白色2x小部件
R.drawable.widget_2x_green, // 绿色2x小部件
R.drawable.widget_2x_red, // 红色2x小部件
};
/**
* 2x
* @param id ID
* @return ID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
// 4x大小小部件背景资源数组
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow, // 黄色4x小部件
R.drawable.widget_4x_blue, // 蓝色4x小部件
R.drawable.widget_4x_white, // 白色4x小部件
R.drawable.widget_4x_green, // 绿色4x小部件
R.drawable.widget_4x_red // 红色4x小部件
};
/**
* 4x
* @param id ID
* @return ID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
/**
*
*
*/
public static class TextAppearanceResources {
// 文本样式资源数组
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal, // 普通文本样式
R.style.TextAppearanceMedium, // 中等文本样式
R.style.TextAppearanceLarge, // 大号文本样式
R.style.TextAppearanceSuper // 超大号文本样式
};
/**
*
* @param id IDTEXT_SMALL/MEDIUM/LARGE/SUPER
* @return ID
*/
public static int getTexAppearanceResource(int id) {
/*
* IDbug
* ID
*/
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,192 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Android相关类库
import android.app.Activity; // 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; // URI处理
import android.os.Bundle; // 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; // 数据工具类
// IO异常处理
import java.io.IOException;
import java.util.Objects;
/**
* Activity
*
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// 成员变量
private long mNoteId; // 当前笔记ID
private String mSnippet; // 笔记摘要内容
private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要预览最大长度
MediaPlayer mPlayer; // 媒体播放器实例
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置无标题栏样式
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取窗口对象并设置标志位
final Window win = getWindow();
// 允许在锁屏界面显示
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 如果屏幕未开启,设置相关标志位
if (!isScreenOn()) {
win.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | // 保持屏幕开启
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | // 点亮屏幕
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | // 允许锁屏
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR // 窗口装饰布局
);
}
// 获取启动Activity的Intent
Intent intent = getIntent();
try {
// 从Intent URI中解析出笔记ID格式content://xxx/notes/1
mNoteId = Long.parseLong(Objects.requireNonNull(intent.getData()).getPathSegments().get(1));
// 通过ContentResolver获取笔记摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 截取摘要前60个字符超出部分添加省略提示
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(); // 笔记不可见则直接结束Activity
}
}
/**
*
* @return boolean
*/
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 | SecurityException | IllegalStateException |
IOException e) {
e.printStackTrace();
}
}
/**
*
*/
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name); // 设置标题为应用名称
dialog.setMessage(mSnippet); // 设置消息内容为笔记摘要
// 确定按钮
dialog.setPositiveButton(R.string.notealert_ok, this);
// 如果屏幕已点亮,添加"进入"按钮
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 显示对话框并设置关闭监听
dialog.show().setOnDismissListener(this);
}
/**
*
*/
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_NEGATIVE) { // "进入"按钮
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW); // 设置动作为查看
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID
startActivity(intent); // 启动笔记编辑Activity
}
}
/**
*
*/
public void onDismiss(DialogInterface dialog) {
stopAlarmSound(); // 停止音效
finish(); // 结束当前Activity
}
/**
*
*/
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop(); // 停止播放
mPlayer.release(); // 释放资源
mPlayer = null; // 置空引用
}
}
}

@ -0,0 +1,101 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Android相关类库
import android.app.AlarmManager; // 系统闹钟服务
import android.app.PendingIntent; // 延时意图,用于延迟执行操作
import android.content.BroadcastReceiver; // 广播接收器基类
import android.content.ContentUris; // Content 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; // 笔记表列名定义
/**
*
*
*/
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; // 提醒日期列索引
/**
* 广
* BOOT_COMPLETEDTIMEZONE_CHANGED广
*
* @param context
* @param intent 广
*/
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前系统时间
long currentDate = System.currentTimeMillis();
// 查询所有未来需要提醒的笔记(类型为普通笔记且提醒日期大于当前时间)
Cursor c = context.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 笔记内容URI
PROJECTION, // 查询的列
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) }, // 参数:当前时间
null); // 无排序要求
// 处理查询结果
if (c != null) {
// 如果查询到数据
if (c.moveToFirst()) {
do {
// 获取笔记的提醒时间
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建闹钟触发时要发送的广播意图
Intent sender = new Intent(context, AlarmReceiver.class);
// 设置数据URI为笔记的完整URIcontent://xxx/notes/[id]
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(COLUMN_ID)));
// 创建PendingIntent用于延迟发送广播
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, // 上下文
0, // requestCode
sender, // 要发送的Intent
0); // flags
// 获取系统闹钟服务
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 设置闹钟RTC_WAKEUP表示即使设备休眠也唤醒CPU
alermManager.set(
AlarmManager.RTC_WAKEUP, // 使用实时时钟并在触发时唤醒设备
alertDate, // 触发时间
pendingIntent); // 触发时要执行的PendingIntent
} while (c.moveToNext()); // 处理下一条记录
}
// 关闭游标释放资源
c.close();
}
}
}

@ -0,0 +1,54 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径,指明该类所在的包位置
package net.micode.notes.ui;
// 导入Android相关类库
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver; // 广播接收器基类
import android.content.Context; // 上下文对象,提供应用环境信息
import android.content.Intent; // 意图对象,用于组件间通信
/**
* 广
* 广
*
* 1. BroadcastReceiver
* 2. AlarmAlertActivity
* 3.
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
* 广
*
*
* @param context Activity
* @param intent URI
*/
@Override
public void onReceive(Context context, @SuppressLint("UnsafeIntentLaunch") Intent intent) {
// 修改Intent的目标组件为AlarmAlertActivity
intent.setClass(context, AlarmAlertActivity.class);
// 添加NEW_TASK标志因为从非Activity上下文启动Activity需要此标志
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动闹钟提醒Activity
context.startActivity(intent);
/// 注意点:
/// 1. 原Intent中已经包含了笔记URI数据在AlarmInitReceiver中设置
/// 2. FLAG_ACTIVITY_NEW_TASK确保可以正确启动Activity
/// 3. 此方法执行时间应尽量短避免ANR应用无响应错误
}
}

@ -0,0 +1,457 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Java工具类
import java.text.DateFormatSymbols; // 用于获取AM/PM本地化字符串
import java.util.Calendar; // 日期时间处理类
// 导入项目资源
import net.micode.notes.R; // 资源ID引用
// 导入Android相关类
import android.content.Context; // 上下文对象
import android.text.format.DateFormat; // 日期格式化工具
import android.view.View; // 基础视图类
import android.widget.FrameLayout; // 帧布局容器
import android.widget.NumberPicker;// 数字选择器控件
/**
*
* AM/PM
*/
public class DateTimePicker extends FrameLayout {
// 默认启用状态
private static final boolean DEFAULT_ENABLE_STATE = true;
// 时间相关常量
private static final int HOURS_IN_HALF_DAY = 12; // 半天的12小时
private static final int HOURS_IN_ALL_DAY = 24; // 全天24小时
private static final int DAYS_IN_ALL_WEEK = 7; // 一周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; // AM/PM选择器
// 状态变量
private final Calendar mDate; // 当前选择的日期时间
private final String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; // 日期显示值数组
private boolean mIsAm; // 是否为上午
private boolean mIs24HourView; // 是否为24小时制
private boolean mIsEnabled = DEFAULT_ENABLE_STATE; // 是否启用
private boolean mInitialising; // 是否正在初始化
// 回调接口
private OnDateTimeChangedListener mOnDateTimeChangedListener;
/**
*
*/
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
// 构造函数(使用当前时间)
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
// 构造函数(指定时间戳)
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
// 主构造函数
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = (getCurrentHourOfDay() < HOURS_IN_HALF_DAY);
// 加载布局
inflate(context, R.layout.datetime_picker, this);
// 初始化日期选择器
mDateSpinner = findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
// 日期变化监听器
// 计算日期差值并更新日历
// 更新日期显示
// 触发回调
NumberPicker.OnValueChangeListener mOnDateChangedListener = (picker, oldVal, newVal) -> {
// 计算日期差值并更新日历
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
updateDateControl(); // 更新日期显示
onDateTimeChanged(); // 触发回调
};
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器
mHourSpinner = findViewById(R.id.hour);
// 小时变化监听器处理12/24小时制的复杂逻辑
// 处理12小时制下的日期变更
// 从PM到AM跨日情况
// 从AM到PM跨日情况
// AM/PM切换点检查
// 处理24小时制下的日期变更
// 23点转到0点次日
// 0点转到23点前一天
// 更新实际小时值考虑AM/PM
// 处理需要变更日期的情况
NumberPicker.OnValueChangeListener mOnHourChangedListener = (picker, oldVal, newVal) -> {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
// 处理12小时制下的日期变更
if (!mIs24HourView) {
// 从PM到AM跨日情况
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;
}
// 从AM到PM跨日情况
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;
}
// AM/PM切换点检查
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();
}
}
// 处理24小时制下的日期变更
else {
// 23点转到0点次日
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
}
// 0点转到23点前一天
else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 更新实际小时值考虑AM/PM
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));
}
};
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器
mMinuteSpinner = findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100); // 长按加速间隔
// 分钟变化监听器处理59->00和00->59的边界情况
// 处理分钟循环滚动情况
// 下一小时
// 上一小时
// 处理AM/PM切换
NumberPicker.OnValueChangeListener mOnMinuteChangedListener = (picker, oldVal, newVal) -> {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
// 处理分钟循环滚动情况
if (oldVal == maxValue && newVal == minValue) {
offset += 1; // 下一小时
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1; // 上一小时
}
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
// 处理AM/PM切换
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
};
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化AM/PM选择器使用本地化字符串
String[] ampmStrings = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(ampmStrings);
// AM/PM变化监听器
// 切换AM/PM状态
// 调整12小时
NumberPicker.OnValueChangeListener mOnAmPmChangedListener = (picker, oldVal, newVal) -> {
mIsAm = !mIsAm; // 切换AM/PM状态
// 调整12小时
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();
};
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 初始化控件状态
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置24小时制模式
set24HourView(is24HourView);
// 设置当前时间
setCurrentDate(date);
// 设置初始启用状态
setEnabled(DEFAULT_ENABLE_STATE);
// 初始化完成
mInitialising = false;
}
// 启用/禁用控件
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) return;
super.setEnabled(enabled);
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
// 获取当前时间戳
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
// 设置当前时间(时间戳)
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)
);
}
// 设置当前时间(各部分)
public void setCurrentDate(int year, int month, int dayOfMonth,
int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
// 获取/设置年份
public int getCurrentYear() { return mDate.get(Calendar.YEAR); }
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) return;
mDate.set(Calendar.YEAR, year);
updateDateControl();
onDateTimeChanged();
}
// 获取/设置月份
public int getCurrentMonth() { return mDate.get(Calendar.MONTH); }
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) return;
mDate.set(Calendar.MONTH, month);
updateDateControl();
onDateTimeChanged();
}
// 获取/设置日
public int getCurrentDay() { return mDate.get(Calendar.DAY_OF_MONTH); }
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) return;
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl();
onDateTimeChanged();
}
// 获取24小时制小时
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
// 获取当前显示的小时考虑12/24小时制
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;
}
}
}
// 设置小时
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) return;
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
if (!mIs24HourView) {
// 处理12小时制下的AM/PM状态
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();
}
// 获取/设置分钟
public int getCurrentMinute() { return mDate.get(Calendar.MINUTE); }
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) return;
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged();
}
// 获取/设置24小时制模式
public boolean is24HourView() { return mIs24HourView; }
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) return;
mIs24HourView = is24HourView;
// 更新AM/PM选择器可见性
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
// 更新小时选择器范围
updateHourControl();
// 设置当前小时(保持实际时间不变)
setCurrentHour(getCurrentHourOfDay());
// 更新AM/PM显示
updateAmPmControl();
}
// 更新日期控件显示值(一周日期范围)
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
// 格式化日期为"MM.dd EEEE"格式(如"07.15 星期一"
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues);
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 中间值(当前日)
mDateSpinner.invalidate(); // 强制重绘
}
// 更新AM/PM控件状态
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
// 更新小时控件范围12/24小时制
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);
}
}
// 设置日期时间变化监听器
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
// 触发日期时间变化回调
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this,
getCurrentYear(), getCurrentMonth(), getCurrentDay(),
getCurrentHourOfDay(), getCurrentMinute());
}
}
}

@ -0,0 +1,145 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Java工具类
import java.util.Calendar; // 日期时间处理类
// 导入项目资源
import net.micode.notes.R; // 资源ID引用
// 导入自定义组件
// 日期时间选择器
// 时间变化监听器
// 导入Android相关类
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; // 日期工具类
/**
*
* DateTimePickerAlertDialog
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 成员变量
private final Calendar mDate = Calendar.getInstance(); // 当前选择的日期时间
private boolean mIs24HourView; // 是否为24小时制
private OnDateTimeSetListener mOnDateTimeSetListener; // 回调监听器
/**
*
*/
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date); // 点击确定时回调
}
/**
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 初始化日期时间选择器
// 内嵌的日期时间选择器
DateTimePicker mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker); // 将选择器添加到对话框
// 设置时间变化监听器
mDateTimePicker.setOnDateTimeChangedListener((view, year, month, dayOfMonth, hourOfDay, 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(DialogInterface.BUTTON_POSITIVE,
context.getString(R.string.datetime_dialog_ok), this);
setButton(DialogInterface.BUTTON_NEGATIVE,
context.getString(R.string.datetime_dialog_cancel),
(OnClickListener)null);
// 根据系统设置初始化24小时制显示
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 初始化标题显示
updateTitle(mDate.getTimeInMillis());
}
/**
* 24
* @param is24HourView true24false12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
// 注意这里未实际传递给DateTimePicker可能需要额外处理
}
/**
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
* @param date
*/
private void updateTitle(long date) {
// 设置日期时间显示格式
int flag = DateUtils.FORMAT_SHOW_YEAR | // 显示年份
DateUtils.FORMAT_SHOW_DATE | // 显示日期
DateUtils.FORMAT_SHOW_TIME; // 显示时间
// 添加24小时制标志注意原代码有错误应该是FORMAT_24HOUR或FORMAT_12HOUR
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
// 设置格式化后的标题
setTitle(DateUtils.formatDateTime(
this.getContext(), // 上下文
date, // 时间戳
flag)); // 格式标志
}
/**
*
*/
@Override
public void onClick(DialogInterface arg0, int arg1) {
// 触发回调事件
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(
this, // 当前对话框实例
mDate.getTimeInMillis()); // 最终选择的时间戳
}
}
}

@ -0,0 +1,89 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Android相关类库
import android.content.Context; // 上下文对象,用于获取系统服务
import android.view.Menu; // 菜单接口
import android.view.MenuItem; // 菜单项接口
// 基础视图类
// 点击监听接口
import android.widget.Button; // 按钮控件
import android.widget.PopupMenu; // 弹出式菜单
import android.widget.PopupMenu.OnMenuItemClickListener; // 菜单项点击监听
// 导入项目资源
import net.micode.notes.R; // 资源ID引用
/**
*
* PopupMenuButton
*/
public class DropdownMenu {
// 成员变量
private final Button mButton; // 触发菜单的按钮
private final PopupMenu mPopupMenu; // 弹出菜单实例
private final Menu mMenu; // 菜单对象
/**
*
* @param context
* @param button
* @param menuId IDR.menu.xxx
*/
public DropdownMenu(Context context, Button button, int menuId) {
// 初始化按钮引用
mButton = button;
// 设置按钮背景(下拉箭头图标)
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建PopupMenu并绑定到按钮
mPopupMenu = new PopupMenu(context, mButton);
// 获取菜单对象
mMenu = mPopupMenu.getMenu();
// 加载菜单布局
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 设置按钮点击事件:显示弹出菜单
mButton.setOnClickListener(v -> {
mPopupMenu.show(); // 显示弹出菜单
});
}
/**
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
*
* @param id IDR.id.xxx
* @return MenuItem
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
/**
*
* @param title
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}
}

@ -0,0 +1,131 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.ui;
// 导入Android相关类库
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; // 资源ID引用
// 导入数据类
import net.micode.notes.data.Notes; // 笔记常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记表列名
/**
*
* CursorAdapter
*/
public class FoldersListAdapter extends CursorAdapter {
// 查询投影列(需要获取的字段)
public static final String [] PROJECTION = {
NoteColumns.ID, // 笔记ID
NoteColumns.SNIPPET // 文件夹名称使用SNIPPET字段存储
};
// 列索引常量
public static final int ID_COLUMN = 0; // ID列索引
public static final int NAME_COLUMN = 1; // 名称列索引
/**
*
* @param context
* @param c
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO: 可在此处添加额外初始化代码
}
/**
*
* @param context
* @param cursor
* @param parent
* @return
*/
@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) {
// 确保视图是FolderListItem类型
if (view instanceof FolderListItem) {
String folderName;
// 判断是否为根文件夹(特殊处理)
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
folderName = context.getString(R.string.menu_move_parent_folder);
} else {
folderName = cursor.getString(NAME_COLUMN);
}
// 调用视图的bind方法设置名称
((FolderListItem) view).bind(folderName);
}
}
/**
*
* @param context
* @param position
* @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);
}
/**
*
*/
private static class FolderListItem extends LinearLayout {
private final TextView mName; // 显示文件夹名称的文本视图
/**
*
* @param context
*/
public FolderListItem(Context context) {
super(context);
// 加载布局文件
inflate(context, R.layout.folder_list_item, this);
// 获取名称文本视图
mName = findViewById(R.id.tv_folder_name);
}
/**
*
* @param name
*/
public void bind(String name) {
mName.setText(name); // 设置文本内容
}
}
}

@ -0,0 +1,256 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义当前类的包路径指明该类属于net.micode.notes.ui 包
package net.micode.notes.ui;
/* 导入Android基础类 */
// Context类提供应用环境信息访问接口
import android.content.Context;
// Rect类表示一个矩形区域用于处理视图边界
import android.graphics.Rect;
// Layout类用于管理文本布局和测量
import android.text.Layout;
// Selection类用于处理文本选择操作
import android.text.Selection;
// Spanned接口表示带有样式的文本
// TextUtils类提供文本处理工具方法
import android.text.TextUtils;
// URLSpan类用于标记文本中的超链接
import android.text.style.URLSpan;
// AttributeSet接口表示XML属性集合
import android.util.AttributeSet;
// Log类提供日志记录功能
import android.util.Log;
/* 导入Android视图相关类 */
// ContextMenu类表示上下文菜单
import android.view.ContextMenu;
// KeyEvent类表示按键事件
import android.view.KeyEvent;
// MenuItem类表示菜单项
// OnMenuItemClickListener接口定义菜单项点击回调
// MotionEvent类表示触摸事件
import android.view.MotionEvent;
// EditText类是基础文本编辑框
/* 导入项目资源 */
// 导入项目R类用于访问资源ID
import net.micode.notes.R;
/* 导入Java集合类 */
// HashMap类实现哈希表结构的Map接口
import java.util.HashMap;
// Map接口表示键值对映射集合
import java.util.Map;
import java.util.Objects;
// 继承自AppCompatEditText的支持兼容的编辑文本框
public class NoteEditText extends androidx.appcompat.widget.AppCompatEditText {
private static final String TAG = "NoteEditText"; // 日志标签
private int mIndex; // 当前编辑框在列表中的索引
private int mSelectionStartBeforeDelete; // 删除操作前的选择开始位置
// 定义支持的URL协议常量
private static final String SCHEME_TEL = "tel:";
private static final String SCHEME_HTTP = "http:";
private static final String SCHEME_EMAIL = "mailto:";
// 创建协议到资源ID的映射表
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<>();
static {
// 初始化协议对应的字符串资源
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话协议
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页协议
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);// 邮件协议
}
/**
* NoteEditActivity
*
*/
public interface OnTextViewChangeListener {
/**
*
* @param index
* @param text
*/
void onEditTextDelete(int index, String text);
/**
*
* @param index
* @param text
*/
void onEditTextEnter(int index, String text);
/**
*
* @param index
* @param hasText
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener; // 文本变化监听器实例
// 第一个构造方法
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 默认索引为0
}
// 设置当前编辑框索引
public void setIndex(int index) {
mIndex = index;
}
// 设置文本变化监听器
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 第二个构造方法
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle); // 调用父类构造方法
}
// 第三个构造方法
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO: 待实现的构造函数
}
// 触摸事件处理
@Override
public boolean onTouchEvent(MotionEvent event) {
// 按下事件处理
if (event.getAction() == 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); // 设置文本选择位置
}
return super.onTouchEvent(event); // 调用父类处理方法
}
// 按键按下事件处理
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER: // 回车键
if (mOnTextViewChangeListener != null) {
return false; // 交给onKeyUp处理
}
break;
case KeyEvent.KEYCODE_DEL: // 删除键
mSelectionStartBeforeDelete = getSelectionStart(); // 记录删除前的位置
break;
default:
break;
}
return super.onKeyDown(keyCode, event); // 调用父类处理方法
}
// 按键释放事件处理
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL: // 删除键
if (mOnTextViewChangeListener != null) {
// 如果在起始位置删除且不是第一个编辑框,则触发删除回调
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, Objects.requireNonNull(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 = Objects.requireNonNull(getText()).subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart)); // 保留光标前的内容
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); // 触发添加回调
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event); // 调用父类处理方法
}
// 焦点变化处理
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
// 通知监听器文本变化状态
if (mOnTextViewChangeListener != null) {
mOnTextViewChangeListener.onTextChange(mIndex, focused || !TextUtils.isEmpty(getText()));
}
super.onFocusChanged(focused, direction, previouslyFocusedRect); // 调用父类方法
}
// 创建上下文菜单
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() != null) {
// 获取当前选择的起止位置
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
// 计算最小和最大位置
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选择范围内的URLSpan对象
final URLSpan[] urls = getText().getSpans(min, max, URLSpan.class);
if (urls.length == 1) { // 如果选中了一个URL
int defaultResId = 0;
// 根据URL协议类型获取对应的资源ID
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().contains(schema)) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 如果没有匹配的协议,使用默认资源
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加上下文菜单项并设置点击监听器
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
item -> {
// 点击时执行URL跳转
urls[0].onClick(NoteEditText.this);
return true;
});
}
}
super.onCreateContextMenu(menu); // 调用父类方法
}
}

@ -0,0 +1,261 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义当前类的包路径属于net.micode.notes.ui 包
package net.micode.notes.ui;
// 导入Android基础类
import android.content.Context; // 上下文对象,提供应用环境信息
import android.database.Cursor; // 数据库查询结果游标
import android.text.TextUtils; // 文本处理工具类
// 导入项目自定义类
import net.micode.notes.data.Contact; // 联系人数据类
import net.micode.notes.data.Notes; // 笔记数据常量定义
import net.micode.notes.data.Notes.NoteColumns; // 笔记表列名定义
import net.micode.notes.tool.DataUtils; // 数据工具类
/**
*
*/
public class NoteItemData {
// 数据库查询列名投影(指定需要查询的列)
static final String[] PROJECTION = new String[] {
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE, // 提醒日期
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建日期
NoteColumns.HAS_ATTACHMENT, // 是否有附件
NoteColumns.MODIFIED_DATE, // 修改日期
NoteColumns.NOTES_COUNT, // 笔记数量(针对文件夹)
NoteColumns.PARENT_ID, // 父项ID
NoteColumns.SNIPPET, // 内容摘要
NoteColumns.TYPE, // 笔记类型
NoteColumns.WIDGET_ID, // 关联小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
};
// 列索引常量定义对应PROJECTION顺序
private static final int ID_COLUMN = 0; // ID列索引
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; // 父项ID列索引
private static final int SNIPPET_COLUMN = 8; // 摘要列索引
private static final int TYPE_COLUMN = 9; // 类型列索引
private static final int WIDGET_ID_COLUMN = 10; // 小部件ID列索引
private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型列索引
/* 笔记数据字段 */
private final long mId; // 笔记唯一ID
private final long mAlertDate; // 提醒时间戳
private final int mBgColorId; // 背景颜色资源ID
private final long mCreatedDate; // 创建时间戳
private final boolean mHasAttachment; // 是否有附件
private final long mModifiedDate; // 最后修改时间戳
private final int mNotesCount; // 包含的笔记数量(文件夹用)
private final long mParentId; // 父文件夹ID
private String mSnippet; // 笔记内容摘要
private final int mType; // 笔记类型(笔记/文件夹等)
private final int mWidgetId; // 关联的小部件ID
private final int mWidgetType; // 小部件类型
/* 联系人相关字段 */
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
*/
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);
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 处理内容摘要,移除复选框标记
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化通话记录相关信息
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 如果是通话记录文件夹中的笔记,获取电话号码
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
if (!TextUtils.isEmpty(mPhoneNumber)) {
// 根据电话号码获取联系人姓名
mName = Contact.getContact(context, mPhoneNumber);
if (mName == null) {
mName = mPhoneNumber; // 如果没有联系人信息,显示号码
}
}
}
// 确保姓名不为null
if (mName == null) {
mName = "";
}
// 检查当前项在列表中的位置状态
checkPostion(cursor);
}
/**
*
* @param cursor
*/
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast(); // 是否是最后一项
mIsFirstItem = cursor.isFirst(); // 是否是第一项
mIsOnlyOneItem = (cursor.getCount() == 1); // 是否是唯一一项
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 如果是笔记类型且不是第一项,检查前一项是否是文件夹
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
int position = cursor.getPosition();
if (cursor.moveToPrevious()) { // 移动到前一项
int prevType = cursor.getInt(TYPE_COLUMN);
// 前一项是文件夹或系统文件夹
if (prevType == Notes.TYPE_FOLDER || prevType == Notes.TYPE_SYSTEM) {
// 判断是多个还是一个笔记跟随文件夹
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
mIsOneNoteFollowingFolder = true;
}
}
// 移动回原来的位置
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
/* ========== 以下是各种访问方法 ========== */
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder; // 是否是单个笔记跟随文件夹
}
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder; // 是否是多个笔记跟随文件夹
}
public boolean isLast() {
return mIsLastItem; // 是否是最后一项
}
public String getCallName() {
return mName; // 获取联系人姓名
}
public boolean isFirst() {
return mIsFirstItem; // 是否是第一项
}
public boolean isSingle() {
return mIsOnlyOneItem; // 是否是唯一一项
}
public long getId() {
return mId; // 获取笔记ID
}
public long getAlertDate() {
return mAlertDate; // 获取提醒时间
}
public long getCreatedDate() {
return mCreatedDate; // 获取创建时间
}
public boolean hasAttachment() {
return mHasAttachment; // 是否有附件
}
public long getModifiedDate() {
return mModifiedDate; // 获取修改时间
}
public int getBgColorId() {
return mBgColorId; // 获取背景颜色ID
}
public long getParentId() {
return mParentId; // 获取父文件夹ID
}
public int getNotesCount() {
return mNotesCount; // 获取包含的笔记数量
}
public long getFolderId() {
return mParentId; // 获取文件夹ID与getParentId相同
}
public int getType() {
return mType; // 获取笔记类型
}
public int getWidgetType() {
return mWidgetType; // 获取小部件类型
}
public int getWidgetId() {
return mWidgetId; // 获取小部件ID
}
public String getSnippet() {
return mSnippet; // 获取内容摘要
}
public boolean hasAlert() {
return (mAlertDate > 0); // 是否有提醒
}
public boolean isCallRecord() {
// 是否是通话记录(在通话记录文件夹中且有号码)
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
*
* @param cursor
* @return
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}

@ -0,0 +1,276 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明当前Java文件的包路径表明该类所在的位置
package net.micode.notes.ui;
// 导入Android相关的Context类用于访问应用资源和系统服务
import android.content.Context;
// 导入Cursor类用于数据库查询结果的遍历
import android.database.Cursor;
// 导入Android日志工具类
import android.util.Log;
// 导入Android基础视图类
import android.view.View;
// 导入视图组类(用于视图的容器管理)
import android.view.ViewGroup;
// 导入CursorAdapter类用于将Cursor数据适配到ListView等控件
import android.widget.CursorAdapter;
// 导入当前应用的数据模型包
import net.micode.notes.data.Notes;
// 导入Java集合框架相关类
import java.util.Collection; // 集合框架的根接口
import java.util.HashMap; // 哈希映射表实现
import java.util.HashSet; // 哈希集合实现
import java.util.Iterator; // 迭代器接口
/**
*
*
* 1.
* 2.
* 3.
*/
public class NotesListAdapter extends CursorAdapter {
private static final String TAG = "NotesListAdapter"; // 日志标签
private final Context mContext; // 上下文对象
private final HashMap<Integer, Boolean> mSelectedIndex; // 存储选中状态的映射表(位置 -> 是否选中)
private int mNotesCount; // 笔记总数(不含文件夹)
private boolean mChoiceMode; // 是否处于多选模式
/**
*
*/
public static class AppWidgetAttribute {
public int widgetId; // 小部件ID
public final ThreadLocal<Integer> widgetType = new ThreadLocal<>() {
@Override
protected Integer initialValue() {
return 0;
}
}; // 小部件类型
}
/**
*
* @param context
*/
public NotesListAdapter(Context context) {
super(context, null); // 初始化CursorAdapter初始cursor为null
mSelectedIndex = new HashMap<>(); // 初始化选中状态集合
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
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked); // 更新选中状态
notifyDataSetChanged(); // 通知视图更新
}
/**
*
* @return true
*/
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 ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<>();
// 遍历所有选中项
for (Integer position : mSelectedIndex.keySet()) {
if (Boolean.TRUE.equals(mSelectedIndex.get(position))) {
long id = getItemId(position); // 获取项ID
// 检查是否为根文件夹(不应该被选中)
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id); // 添加到结果集
}
}
}
return itemSet;
}
/**
*
* @return null
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<>();
// 遍历所有选中项
for (Integer position : mSelectedIndex.keySet()) {
if (Boolean.TRUE.equals(mSelectedIndex.get(position))) {
Cursor c = (Cursor) getItem(position); // 获取项数据
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId(); // 设置小部件ID
widget.widgetType.set(item.getWidgetType()); // 设置小部件类型
itemSet.add(widget); // 添加到结果集
/*
* cursor
*/
} else {
Log.e(TAG, "Invalid cursor");
return null; // 遇到错误返回null
}
}
}
return itemSet;
}
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
Iterator<Boolean> iter = values.iterator();
int count = 0;
// 统计选中状态为true的数量
while (iter.hasNext()) {
if (iter.next()) {
count++;
}
}
return count;
}
/**
*
* @return true
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
// 选中数量不为0且等于总笔记数量
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
* @param position
* @return true
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false; // 未记录的位置默认为未选中
}
return Boolean.TRUE.equals(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) {
// 只统计笔记类型TYPE_NOTE
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++; // 计数增加
}
} else {
Log.e(TAG, "Invalid cursor");
return; // 遇到错误提前退出
}
}
}
}

@ -0,0 +1,183 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明当前Java文件所在的包路径net.micode.notes.ui 表示笔记应用的UI层
package net.micode.notes.ui;
// Android注解库@SuppressLint用于忽略特定Lint警告
import android.annotation.SuppressLint;
// Android上下文类提供应用环境信息和访问系统服务
import android.content.Context;
// Android日期/时间格式化工具类,用于友好显示时间
import android.text.format.DateUtils;
// Android基础视图类所有UI组件的基类
import android.view.View;
// 复选框控件,用于多选操作
import android.widget.CheckBox;
// 图片视图控件,用于显示图标
import android.widget.ImageView;
// 线性布局容器,用于水平或垂直排列子视图
import android.widget.LinearLayout;
// 文本视图控件,用于显示文字内容
import android.widget.TextView;
// 导入应用R资源文件自动生成包含所有资源ID
import net.micode.notes.R;
// 导入笔记数据模型和常量定义
import net.micode.notes.data.Notes;
// 导入数据工具类,提供数据处理方法
import net.micode.notes.tool.DataUtils;
// 导入笔记项背景资源定义类
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
/**
*
* LinearLayout/
*/
public class NotesListItem extends LinearLayout {
// 提醒图标(显示时钟或通话记录图标)
private final ImageView mAlert;
// 标题/内容文本视图
private final TextView mTitle;
// 时间文本视图(显示相对时间)
private final TextView mTime;
// 通话记录联系人名称(仅通话记录可见)
private final TextView mCallName;
// 当前绑定的笔记数据对象
private NoteItemData mItemData;
// 多选模式下的复选框
private final CheckBox mCheckBox;
/**
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
// 加载布局文件R.layout.note_item )到当前视图
inflate(context, R.layout.note_item, this);
// 初始化视图组件
mAlert = findViewById(R.id.iv_alert_icon); // 提醒图标
mTitle = findViewById(R.id.tv_title); // 标题文本
mTime = findViewById(R.id.tv_time); // 时间文本
mCallName = findViewById(R.id.tv_name); // 联系人名称
mCheckBox = findViewById(android.R.id.checkbox);// 多选复选框
}
/**
*
* @param context
* @param data
* @param choiceMode
* @param checked
*/
@SuppressLint("SetTextI18n") // 忽略国际化的Lint警告
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);
}
}
}
// 设置相对时间显示(如"2分钟前"
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 设置背景样式
setBackground(data);
}
/**
*
* @param data
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId(); // 获取背景颜色ID
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
*/
public NoteItemData getItemData() {
return mItemData;
}
}

@ -0,0 +1,452 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 声明当前Java文件的包路径该Activity位于notes应用的ui包下
package net.micode.notes.ui;
// Android账户管理相关类
import android.accounts.Account; // 账户基本类
import android.accounts.AccountManager; // 账户管理系统服务
// Android基础UI组件
// 顶部操作栏
import android.app.AlertDialog; // 提示对话框
import android.content.BroadcastReceiver; // 广播接收器基类
import android.content.ContentValues; // 键值对存储(用于数据库操作)
// Android核心组件类
import android.content.Context; // 应用上下文
// 对话框按钮回调接口
import android.content.Intent; // 意图用于Activity跳转
import android.content.IntentFilter; // 意图过滤器(广播相关)
import android.content.SharedPreferences; // 轻量级数据存储
// Android基础框架类
import android.os.Bundle; // 数据存储Bundle
// 设置相关类Preference
import android.preference.Preference; // 设置项基类
// 设置项点击监听
import android.preference.PreferenceActivity; // 设置Activity基类
import android.preference.PreferenceCategory; // 设置分组容器
// Android文本工具类
import android.text.TextUtils; // 文本处理工具
import android.text.format.DateFormat; // 日期格式化
// Android视图相关类
import android.view.LayoutInflater; // 布局加载器
// 菜单
import android.view.MenuItem; // 菜单项
import android.view.View; // 基础视图类
import android.widget.Button; // 按钮控件
import android.widget.TextView; // 文本控件
import android.widget.Toast; // Toast提示
// 应用资源类
import net.micode.notes.R; // 自动生成的资源ID类
// 本地数据模型
import net.micode.notes.data.Notes; // 笔记数据常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记数据列名
// 同步服务相关
import net.micode.notes.gtask.remote.GTaskSyncService; // 谷歌任务同步服务
// Java工具类
import java.util.Objects; // 对象工具类(判空等)
/**
* ActivityPreferenceActivity
*
* 1. Google
* 2.
* 3. /
*/
public class NotesPreferenceActivity extends PreferenceActivity {
// 偏好设置文件名
public static final String PREFERENCE_NAME = "notes_preferences";
// 同步账户名的偏好设置键
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
// 最后同步时间的偏好设置键
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
// 背景颜色设置的偏好设置键
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
// 同步账户设置项的键
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
// 账户权限过滤键
private static final String AUTHORITIES_FILTER_KEY = "authorities";
// 账户设置分类
private PreferenceCategory mAccountCategory;
// 同步服务广播接收器
private GTaskReceiver mReceiver;
// 原始账户数组(用于检测新增账户)
private Account[] mOriAccounts;
// 是否添加了新账户的标志
private boolean mHasAddedAccount;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 设置ActionBar显示返回箭头
Objects.requireNonNull(getActionBar()).setDisplayHomeAsUpEnabled(true);
// 从XML加载偏好设置
addPreferencesFromResource(R.xml.preferences);
// 获取账户设置分类
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// 初始化广播接收器
mReceiver = new GTaskReceiver();
// 创建意图过滤器,监听同步服务广播
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
// 初始化原始账户数组
mOriAccounts = null;
// 加载设置页面的顶部自定义布局
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
}
@Override
protected void onResume() {
super.onResume();
// 如果用户添加了新账户,需要自动设置同步账户
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;
}
}
}
}
// 刷新UI状态
refreshUI();
}
@Override
protected void onDestroy() {
// 注销广播接收器
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
*/
private void loadAccountPreference() {
// 清空现有设置项
mAccountCategory.removeAll();
// 创建账户设置项
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
// 设置标题和摘要
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
// 设置点击监听器
accountPref.setOnPreferenceClickListener(preference -> {
if (!GTaskSyncService.isSyncing()) {
// 首次设置账户
if (TextUtils.isEmpty(defaultAccount)) {
showSelectAccountAlertDialog();
} else {
// 已有账户时显示确认对话框
showChangeAccountConfirmAlertDialog();
}
} else {
// 同步进行中不能更改账户
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account,
Toast.LENGTH_SHORT).show();
}
return true;
});
// 添加设置项到分类
mAccountCategory.addPreference(accountPref);
}
/**
*
*/
private void loadSyncButton() {
Button syncButton = findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = findViewById(R.id.prefenerece_sync_status_textview);
// 根据同步状态设置按钮文本和点击事件
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(v -> GTaskSyncService.cancelSync(NotesPreferenceActivity.this));
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(v -> GTaskSyncService.startSync(NotesPreferenceActivity.this));
}
// 只有设置了同步账户才能点击同步按钮
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 设置最后同步时间显示
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
/**
* UI
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
*
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载自定义标题布局
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
// 获取当前所有Google账户
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
// 构建账户列表项
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
// 标记当前已选账户
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
// 设置单选列表
dialogBuilder.setSingleChoiceItems(items, checkedItem,
(dialog, 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(v -> {
mHasAddedAccount = true;
// 启动添加账户设置界面
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[]{"gmail-ls"});
startActivityForResult(intent, -1);
dialog.dismiss();
});
}
/**
*
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载自定义标题布局
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = 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, (dialog, which) -> {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
});
dialogBuilder.show();
}
/**
* Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
// 保存账户到偏好设置
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, Objects.requireNonNullElse(account, ""));
editor.apply();
// 重置最后同步时间
setLastSyncTime(this, 0);
// 在新线程中清除本地同步信息
new Thread(() -> {
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(this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
// 移除账户相关设置
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.apply();
// 在新线程中清除本地同步信息
new Thread(() -> {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}).start();
}
/**
*
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.apply();
}
/**
*
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广
*/
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 收到同步状态变化时刷新UI
refreshUI();
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
/**
*
*/
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
// 点击返回箭头时回到笔记列表
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
}
return false;
}
}

@ -0,0 +1,208 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.widget;
// 导入Android相关类
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; // 远程视图用于小部件UI
// 导入项目资源
import net.micode.notes.R; // 项目资源文件
// 导入项目数据类
import net.micode.notes.data.Notes; // 笔记常量
import net.micode.notes.data.Notes.NoteColumns; // 笔记列定义
// 导入工具类
import net.micode.notes.tool.ResourceParser; // 资源解析工具
// 导入Activity类
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, // 笔记ID
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.SNIPPET // 笔记摘要
};
// 投影列索引常量
public static final int COLUMN_ID = 0; // ID列索引
public static final int COLUMN_BG_COLOR_ID = 1; // 背景颜色列索引
public static final int COLUMN_SNIPPET = 2; // 摘要列索引
private static final String TAG = "NoteWidgetProvider"; // 日志标签
/**
*
* @param context
* @param appWidgetIds ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 创建内容值对象,用于更新数据库
ContentValues values = new ContentValues();
// 将笔记的小部件ID设为无效值
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// 遍历所有被删除的小部件ID
for (int appWidgetId : appWidgetIds) {
// 更新数据库,解除笔记与小部件的关联
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?", // 更新条件
new String[]{String.valueOf(appWidgetId)}); // 参数值
}
}
/**
*
* @param context
* @param widgetId ID
* @return
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
// 查询数据库获取关联的笔记信息
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION, // 查询列
// 查询条件匹配小部件ID且不在回收站中
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null); // 无排序
}
/**
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false); // 默认非隐私模式
}
/**
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
// 遍历所有需要更新的小部件
/*
宿Activity
*/
for (int appWidgetId : appWidgetIds) {
// 检查小部件ID是否有效
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
int bgId = ResourceParser.getDefaultBgId(context); // 获取默认背景ID
String snippet; // 笔记摘要
// 创建编辑笔记的Intent
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 设置启动模式
// 添加小部件ID到Intent
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetId);
// 添加小部件类型到Intent
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 获取关联的笔记信息
Cursor c = getNoteWidgetInfo(context, appWidgetId);
if (c != null && c.moveToFirst()) {
// 检查是否有多条笔记关联同一个小部件(异常情况)
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetId);
c.close();
return;
}
// 从游标获取笔记信息
snippet = c.getString(COLUMN_SNIPPET); // 获取摘要
bgId = c.getInt(COLUMN_BG_COLOR_ID); // 获取背景ID
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); // 添加笔记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));
// 添加背景ID到Intent
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/*
宿Activity
*/
PendingIntent pendingIntent;
if (privacyMode) {
// 隐私模式下显示特殊文本
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
// 点击跳转到笔记列表
pendingIntent = PendingIntent.getActivity(context, appWidgetId, new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 正常模式显示笔记摘要
rv.setTextViewText(R.id.widget_text, snippet);
// 点击跳转到笔记编辑界面
pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
// 设置点击事件
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 更新小部件
appWidgetManager.updateAppWidget(appWidgetId, rv);
}
}
}
/**
* ID
* @param bgId ID
* @return ID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
* @return ID
*/
protected abstract int getLayoutId();
/**
*
* @return
*/
protected abstract int getWidgetType();
}

@ -0,0 +1,77 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径
package net.micode.notes.widget;
// 导入Android相关类
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; // 资源解析工具类
/**
* 2x
* NoteWidgetProvider2x
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
/**
*
*
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 调用父类的update方法进行基础更新
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* ID
*
* @return 2xID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_2x; // 使用widget_2x布局文件
}
/**
* ID2x
*
* @param bgId IDResourceParser.YELLOW
* @return 2xID
*/
@Override
protected int getBgResourceId(int bgId) {
// 通过ResourceParser获取2x尺寸的小部件背景资源
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
/**
*
*
* @return 2x
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X; // 返回2x小部件类型常量
}
}

@ -0,0 +1,82 @@
/*
* (c) 2010-2011MiCode (www.micode.net)
* Apache 2.0 "许可证"
* 使
*
* http://www.apache.org/licenses/LICENSE-2.0
* "原样"
*
*
* Apache2.0
*/
// 定义包路径说明该类所属的Java包
package net.micode.notes.widget;
// 导入Android系统类库
import android.appwidget.AppWidgetManager; // 小部件管理类用于管理App Widgets
import android.content.Context; // 上下文类,提供应用环境信息
// 导入项目资源
import net.micode.notes.R; // 自动生成的R.java 文件包含所有资源ID
// 导入项目数据类
import net.micode.notes.data.Notes; // 笔记应用的常量定义
// 导入工具类
import net.micode.notes.tool.ResourceParser; // 资源解析工具类
/**
* 4x
* NoteWidgetProvider4x
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
/**
*
* 4x
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 直接调用父类的update方法完成基础更新操作
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* 4xID
* 4x
*
* @return int 4xIDR.layout.widget_4x
*/
@Override
protected int getLayoutId() {
// 返回4x小部件专用的布局文件ID
return R.layout.widget_4x;
}
/**
* ID4x
* 4x
*
* @param bgId IDResourceParser
* @return int 4xID
*/
@Override
protected int getBgResourceId(int bgId) {
// 通过ResourceParser工具类获取4x尺寸的背景资源
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
/**
*
* 4x
*
* @return int 4xNotes.TYPE_WIDGET_4X
*/
@Override
protected int getWidgetType() {
// 返回4x小部件的类型定义常量
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

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

Loading…
Cancel
Save