Compare commits

..

20 Commits

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

@ -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="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

@ -0,0 +1,20 @@
<?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="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</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,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<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,150 @@
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.micode.notes"
android:versionCode="1"
android:versionName="0.1" >
<uses-sdk android:minSdkVersion="14" />
<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: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: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: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: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" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<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,62 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "net.micode.notes"
compileSdk = 34
defaultConfig {
applicationId = "net.micode.notes"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
packaging() {
resources.excludes.add("META-INF/DEPENDENCIES")
resources.excludes.add("META-INF/NOTICE")
resources.excludes.add("META-INF/LICENSE")
resources.excludes.add("META-INF/LICENSE.txt")
resources.excludes.add("META-INF/NOTICE.txt")
}
}
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
// implementation(fileTree(mapOf(
// "dir" to "D:\\android\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib",
// "include" to listOf("*.aar", "*.jar"),
// "exclude" to listOf()
// )))
implementation(files("D:\\android\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib\\httpclient-osgi-4.5.14.jar"))
implementation(files("D:\\android\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib\\httpclient-win-4.5.14.jar"))
implementation(files("D:\\android\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib\\httpcore-4.4.16.jar"))
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,26 @@
package net.micode.notes;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("net.micode.notes", appContext.getPackageName());
}
}

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
tools:targetApi="31">
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true" />
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_2x_info" />
</receiver>
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_4x_info" />
</receiver>
<receiver android:name=".ui.AlarmInitReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<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" />
<!-- <activity-->
<!-- android:name=".MainActivity"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
</application>
</manifest>

@ -0,0 +1,24 @@
package net.micode.notes;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
}

@ -0,0 +1,92 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import java.util.HashMap;
// Contact类主要用于根据电话号码查询对应的联系人姓名通过缓存机制来提高查询效率避免重复查询相同电话号码对应的联系人信息。
public class Contact {
// 用于缓存已经查询过的电话号码与对应的联系人姓名的映射关系,以减少重复查询数据库的操作,提高性能。
private static HashMap<String, String> sContactCache;
// 用于日志记录的标签方便在Log输出中识别该类相关的日志信息便于调试和查看运行情况。
private static final String TAG = "Contact";
// 定义一个用于查询联系人的SQL语句模板该语句通过电话号码来匹配联系人涉及到一些联系人数据库表如Data表、phone_lookup表等中的字段和条件判断
// 目的是找到与给定电话号码匹配的联系人记录具体是获取联系人的显示名称。其中使用了自定义函数PHONE_NUMBERS_EQUAL来比较电话号码是否相等
// 并且对电话号码进行了一定的格式化处理通过min_match条件等以更准确地匹配。
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
// 静态方法,用于根据给定的电话号码获取对应的联系人姓名,是该类的核心功能方法。
public static String getContact(Context context, String phoneNumber) {
// 如果联系人缓存为空首次使用或者缓存被清空等情况则创建一个新的HashMap实例用于缓存联系人信息。
if (sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
// 先检查缓存中是否已经存在该电话号码对应的联系人姓名,如果存在则直接从缓存中获取并返回,避免再次查询数据库,提高效率。
if (sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
// 根据给定的电话号码对查询语句中的占位符进行替换将电话号码格式化为适合数据库查询的形式使用PhoneNumberUtils的方法进行格式化
// 得到最终用于查询的SQL语句字符串。
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
// 通过上下文Context对象获取内容解析器ContentResolver并使用其query方法执行数据库查询操作
// 查询联系人数据库Data.CONTENT_URI表示联系人数据的内容提供者的统一资源标识符
// 指定只获取联系人的显示名称Phone.DISPLAY_NAME字段传入查询条件格式化后的查询语句和电话号码参数排序等参数设为null。
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String[]{Phone.DISPLAY_NAME},
selection,
new String[]{phoneNumber},
null);
// 如果查询到的游标Cursor不为空且游标能够移动到第一条记录表示查询到了匹配的联系人数据则进行以下操作。
if (cursor!= null && cursor.moveToFirst()) {
try {
// 从游标中获取第一条记录的第一个字段(也就是联系人的显示名称),并将该电话号码和对应的联系人姓名存入缓存中,方便后续查询使用,最后返回获取到的联系人姓名。
String name = cursor.getString(0);
sContactCache.put(phoneNumber, name);
return name;
} catch (IndexOutOfBoundsException e) {
// 如果在获取游标中字符串数据时发生越界异常比如可能查询结果的结构不符合预期等情况则记录错误日志并返回null表示获取联系人姓名失败。
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
// 无论是否成功获取到联系人姓名,都需要关闭游标,释放相关资源,避免内存泄漏等问题。
cursor.close();
}
} else {
// 如果游标为空或者游标中没有可移动到的记录即没有查询到匹配的联系人数据则记录一条调试日志表示没有找到与该电话号码匹配的联系人然后返回null。
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

@ -0,0 +1,166 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.net.Uri;
// Notes类主要用于定义笔记相关的数据结构、常量以及接口等它在整个笔记应用的数据管理方面起着核心作用
// 涵盖了笔记类型、系统文件夹标识、各种用于传递数据的Intent额外参数、内容提供者的Uri以及笔记和数据相关的列定义等内容。
public class Notes {
// 定义内容提供者的授权字符串用于标识该应用的笔记数据内容提供者在使用内容解析器ContentResolver访问相关数据时需要用到。
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;
public static final int ID_TEMPARAY_FOLDER = -1;
public static final int ID_CALL_RECORD_FOLDER = -2;
public static final int ID_TRASH_FOLER = -3;
// 定义一系列用于在Intent中传递额外数据的键常量方便在不同组件如Activity、Service等之间传递与笔记相关的特定数据
// 例如提醒日期、背景颜色ID、桌面小部件相关信息、文件夹ID、通话日期等。
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
// 定义桌面小部件的不同类型常量,用于区分不同规格或样式的桌面小部件,便于在处理桌面小部件相关逻辑时进行分类操作。
public static final int TYPE_WIDGET_INVALIDE = -1;
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
// 内部静态类DataConstants目前看起来是用于汇总定义笔记相关的不同内容类型的常量方便统一管理和引用
// 这里包含了普通文本笔记和通话记录笔记的内容类型常量具体使用场景可能涉及到内容提供者的MIME类型判断等
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
* UriUriContentResolver
* content://的规范指定了对应的授权AUTHORITY和路径/note
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri
* /data
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
// 内部接口NoteColumns用于定义笔记相关数据表中的列名常量清晰地规范了笔记数据在数据库存储时各个字段的名称和对应的数据类型
// 方便在数据库操作(如查询、插入、更新等)时进行统一引用,避免硬编码列名导致的错误和维护困难。
public interface NoteColumns {
// 笔记或文件夹的唯一标识列,类型为长整型整数,用于在数据库中唯一区分每条笔记或文件夹记录。
public static final String ID = "_id";
// 笔记或文件夹的父级ID列类型为长整型整数通过该字段可以构建笔记的层级结构比如文件夹包含哪些子笔记等关系。
public static final String PARENT_ID = "parent_id";
// 笔记或文件夹的创建日期列,类型为长整型整数,用于记录数据创建的时间戳,方便进行时间相关的排序、筛选等操作。
public static final String CREATED_DATE = "created_date";
// 笔记或文件夹的最后修改日期列,类型为长整型整数,记录数据最后一次被修改的时间戳,常用于判断数据是否更新等逻辑。
public static final String MODIFIED_DATE = "modified_date";
// 提醒日期列,类型为长整型整数,用于设置和获取笔记的提醒时间相关功能(比如提醒用户查看笔记等情况)。
public static final String ALERTED_DATE = "alert_date";
// 对于文件夹表示文件夹名称对于笔记表示文本内容摘要等类型为文本TEXT方便展示和快速了解笔记或文件夹的基本信息。
public static final String SNIPPET = "snippet";
// 笔记的桌面小部件ID列类型为长整型整数用于关联笔记与对应的桌面小部件可能在桌面小部件显示笔记相关内容等场景使用。
public static final String WIDGET_ID = "widget_id";
// 笔记的桌面小部件类型列类型为长整型整数结合上面的小部件ID可以更准确地处理不同类型桌面小部件与笔记的关联和展示等操作。
public static final String WIDGET_TYPE = "widget_type";
// 笔记的背景颜色ID列类型为长整型整数用于设置和获取笔记在列表展示等场景下的背景颜色相关属性。
public static final String BG_COLOR_ID = "bg_color_id";
// 用于标记笔记是否有附件对于文本笔记通常为0无附件对于多媒体笔记则为1有附件类型为整数方便判断笔记是否包含额外的附件资源。
public static final String HAS_ATTACHMENT = "has_attachment";
// 文件夹中包含的笔记数量列,类型为长整型整数,便于快速获取文件夹下的笔记数量情况,进行统计或展示相关信息。
public static final String NOTES_COUNT = "notes_count";
// 表示数据的类型(是笔记还是文件夹等),类型为整数,通过该字段可以在代码逻辑中对不同类型的数据进行相应的处理和操作。
public static final String TYPE = "type";
// 最后同步ID列类型为长整型整数可能用于记录笔记与外部同步如与云端同步等情况的相关标识信息便于跟踪同步状态和版本等情况。
public static final String SYNC_ID = "sync_id";
// 标记本地是否有修改的标志列,类型为整数,用于判断笔记在本地是否被修改过,以便决定是否需要进行同步等后续操作。
public static final String LOCAL_MODIFIED = "local_modified";
// 笔记在移动到临时文件夹之前的原始父级ID列类型为整数可能在笔记移动操作等相关逻辑中用于恢复原层级关系等情况。
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
// 与gtask可能是一种任务管理相关的外部服务等具体取决于应用场景相关的ID列类型为文本TEXT用于关联笔记与外部任务管理系统等功能交互。
public static final String GTASK_ID = "gtask_id";
// 版本代码列,类型为长整型整数,用于记录笔记数据的版本信息,便于进行数据更新、兼容等相关处理。
public static final String VERSION = "version";
}
// 内部接口DataColumns同样用于定义数据相关数据表中的列名常量不过这里更侧重于笔记具体数据内容相关的字段定义
// 比如MIME类型、所属笔记ID、创建和修改日期、具体内容以及一些通用的数据字段其含义根据MIME类型不同而有所变化
// 用于规范和统一管理笔记详细数据在数据库中的存储结构。
public interface DataColumns {
// 数据行的唯一标识列,类型为长整型整数,用于在数据相关的表中唯一区分每条数据记录。
public static final String ID = "_id";
// 数据项的MIME类型列类型为文本TEXT用于标识该数据的具体格式或类型如文本、图片、音频等不同类型的数据方便进行相应的处理和展示逻辑。
public static final String MIME_TYPE = "mime_type";
// 该数据所属笔记的引用ID列类型为长整型整数通过这个字段可以建立数据与对应的笔记之间的关联关系明确数据是属于哪条笔记的。
public static final String NOTE_ID = "note_id";
// 数据的创建日期列,类型为长整型整数,记录数据创建的时间戳,用于时间相关的排序、统计等操作。
public static final String CREATED_DATE = "created_date";
// 数据的最后修改日期列,类型为长整型整数,记录数据最后一次被修改的时间戳,便于判断数据的更新情况。
public static final String MODIFIED_DATE = "modified_date";
// 数据的具体内容列类型为文本TEXT用于存储和获取数据的实际文本内容比如笔记的正文内容等情况
public static final String CONTENT = "content";
// 通用数据列其含义根据MIME类型特定用于整数数据类型的情况类型为整数提供一种灵活的方式来存储不同MIME类型相关的整数数据。
public static final String DATA1 = "data1";
// 通用数据列其含义根据MIME类型特定用于整数数据类型的情况类型为整数与DATA1类似可用于存储不同的整数相关数据。
public static final String DATA2 = "data2";
// 通用数据列其含义根据MIME类型特定用于文本数据类型的情况类型为文本TEXT用于存储和获取不同MIME类型相关的文本数据。
public static final String DATA3 = "data3";
// 通用数据列其含义根据MIME类型特定用于文本数据类型的情况类型为文本TEXT与DATA3类似可用于存储不同的文本相关数据。
public static final String DATA4 = "data4";
// 通用数据列其含义根据MIME类型特定用于文本数据类型的情况类型为文本TEXT同样用于存储不同的文本相关数据。
public static final String DATA5 = "data5";
}
// 内部静态类TextNote继承自DataColumns接口用于定义文本笔记相关的特定属性和常量比如文本笔记的模式是否是清单模式等
// 内容类型以及对应的内容提供者的Uri等专门针对文本笔记这种特定的数据类型进行相关规范和扩展定义。
public static final class TextNote implements DataColumns {
// 用于指示文本笔记是否处于清单模式的字段类型为整数1表示清单模式0表示正常模式方便在处理文本笔记展示、编辑等逻辑时进行相应的操作。
public static final String MODE = DATA1;
public static final int MODE_CHECK_LIST = 1;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
// 内部静态类CallNote同样继承自DataColumns接口用于定义通话记录笔记相关的特定属性和常量比如通话日期、电话号码、
// 内容类型以及对应的内容提供者的Uri等针对通话记录这种特殊的笔记类型进行详细的规范和定义便于在应用中对通话记录笔记进行相关操作。
public static final class CallNote implements DataColumns {
// 通话记录笔记的通话日期字段,类型为长整型整数,用于记录通话发生的时间,方便展示、查询等操作。
public static final String CALL_DATE = DATA1;
// 通话记录笔记的电话号码字段类型为文本TEXT用于存储通话对方的电话号码信息便于展示和相关功能实现如回拨等
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}

@ -0,0 +1,426 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
// NotesDatabaseHelper类继承自SQLiteOpenHelper用于管理笔记应用的数据库相关操作
// 包括数据库的创建、表结构定义、各种触发器的创建与管理以及数据库版本升级等功能,是整个笔记数据持久化存储的核心辅助类。
public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 定义数据库文件名用于指定应用使用的SQLite数据库的名称后续在创建和打开数据库时会用到这个名称。
private static final String DB_NAME = "note.db";
// 定义数据库版本号,用于标识数据库的结构版本,当数据库结构发生变化(如添加、修改表结构或字段等)时,需要更新这个版本号,
// 以便在onUpgrade方法中执行相应的升级逻辑。
private static final int DB_VERSION = 4;
// 内部接口TABLE用于定义数据库中不同表的名称常量方便在代码中统一引用表名避免硬编码表名导致的错误同时也便于后续对表相关操作的维护和扩展。
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;
// 定义创建笔记表note表的SQL语句字符串详细指定了表中各个字段的名称、数据类型以及默认值等信息
// 这些字段涵盖了笔记的各种属性如ID、父级ID、提醒日期、背景颜色ID、创建日期、是否有附件、修改日期、包含笔记数量、摘要内容、类型、小部件相关信息、同步ID、本地修改标记、原始父级ID、gtask相关ID、版本号等
// 通过这条SQL语句可以创建用于存储笔记基本信息的表结构。
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
// 定义创建数据表data表的SQL语句字符串同样详细指定了表中各个字段的名称、数据类型以及默认值等信息
// 这些字段涉及数据的唯一标识、MIME类型、所属笔记ID、创建日期、修改日期、具体内容以及一些通用的数据字段其含义根据MIME类型不同而有所变化
// 用于存储笔记相关的详细数据内容(比如文本内容、附件信息等不同类型的数据)。
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
// 定义创建基于数据表data表中笔记ID字段NoteColumns.NOTE_ID的索引的SQL语句字符串
// 通过创建索引可以提高根据笔记ID查询相关数据的效率优化数据库查询性能。
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* SQLIDNoteColumns.PARENT_IDNoteColumns.NOTES_COUNT
*
*/
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";
/**
* SQLIDNoteColumns.PARENT_IDNoteColumns.NOTES_COUNT
*
* 0
*/
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";
/**
* SQLNoteColumns.NOTES_COUNT
*
*/
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";
/**
* SQLNoteColumns.NOTES_COUNT
*
* 0
*/
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";
/**
* SQLDataConstants.NOTENoteColumns.SNIPPET
*
*/
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";
/**
* SQLDataConstants.NOTENoteColumns.SNIPPET
*
*/
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";
/**
* SQLDataConstants.NOTENoteColumns.SNIPPET
*
*/
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";
/**
* SQL
*
*/
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";
/**
* SQL
*
*/
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";
/**
* SQLNotes.ID_TRASH_FOLER
* ID
*/
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";
// 构造函数调用父类SQLiteOpenHelper的构造函数传入应用上下文、数据库名称、游标工厂这里设为null以及数据库版本号等参数
// 用于初始化数据库帮助类实例,以便后续进行数据库相关操作。
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 用于创建笔记表note表的方法接收一个SQLiteDatabase实例代表数据库连接对象
// 通过执行之前定义的创建表的SQL语句CREATE_NOTE_TABLE_SQL创建笔记表然后调用方法重新创建笔记表相关的触发器
// 最后调用方法创建系统文件夹(如通话记录文件夹、根文件夹、临时文件夹、回收站文件夹等),并记录一条日志表示笔记表已创建成功。
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
// 用于重新创建笔记表相关触发器的私有方法,先删除已存在的相关触发器(如果有的话),避免重复创建或因之前的触发器逻辑变更导致冲突等问题,
// 然后按照定义好的各个触发器的SQL语句依次
// 用于重新创建笔记表相关触发器的私有方法
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");
// 再按照之前定义好的各个触发器的SQL语句依次重新创建这些触发器确保数据库中的触发器逻辑是最新且符合当前业务需求的
// 以此来保证在笔记表相关数据发生变化(如更新、插入、删除操作)时,能通过这些触发器自动完成与之关联的数据更新、维护操作,保持数据的一致性和完整性
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);
}
// 用于创建系统文件夹的私有方法通过向笔记表TABLE.NOTE插入相应记录来创建不同功能的系统文件夹
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
// 创建用于存放通话记录笔记的文件夹记录
/**
* call record foler for call notes
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
// 创建默认的根文件夹记录
/**
* root folder which is default folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
// 创建用于临时存放笔记的文件夹记录(比如笔记在移动过程中的临时存放位置等情况会用到)
/**
* temporary folder which is used for moving note
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
// 创建回收站文件夹记录,用于存放被删除的笔记等情况
/**
* create trash folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
// 用于创建数据data表的方法接收数据库连接对象SQLiteDatabase实例在数据库中创建数据data
// 并重新创建数据data表相关的触发器同时创建基于笔记ID的索引最后记录一条日志表示数据data表已创建成功
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
// 用于重新创建数据data表相关触发器的私有方法先删除已存在的数据表相关触发器如果有的话
// 然后按照定义好的数据表触发器的SQL语句重新创建它们确保数据data表在数据变化时能通过触发器正确维护与之关联的数据一致性
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);
}
// 采用单例模式获取NotesDatabaseHelper实例的静态同步方法确保在多线程环境下整个应用中只会创建一个该类的实例
// 方便统一管理数据库操作,避免多次创建数据库连接等资源浪费情况
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
// 重写SQLiteOpenHelper的onCreate方法在数据库首次创建时被调用
// 该方法内部调用了创建笔记表createNoteTable和创建数据datacreateDataTable的方法
// 完成初始化创建数据库表结构以及相关触发器、系统文件夹等基础操作
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
// 重写SQLiteOpenHelper的onUpgrade方法用于处理数据库版本升级的逻辑根据旧版本号oldVersion和新版本号newVersion来执行相应的升级操作
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
// 如果旧版本号是1说明需要从版本1升级到更高版本调用upgradeToV2方法进行升级操作
// 同时标记skipV2为true表示这次升级包含了从版本2到版本3的升级情况可能是升级逻辑合并等原因然后将旧版本号加1
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
// 如果旧版本号是2且skipV2为false即不是从版本1升级过来包含了这一步的情况
// 则调用upgradeToV3方法进行从版本2到版本3的升级操作标记reCreateTriggers为true表示需要重新创建相关触发器然后将旧版本号加1
if (oldVersion == 2 &&!skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
// 如果旧版本号是3调用upgradeToV4方法进行从版本3到版本4的升级操作然后将旧版本号加1
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 如果reCreateTriggers为true说明需要重新创建笔记表和数据data表相关的触发器调用相应方法进行触发器的重新创建操作
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 如果最终旧版本号和新版本号不一致,说明升级过程出现问题,抛出异常表示数据库升级到指定新版本失败
if (oldVersion!= newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
// 用于将数据库从版本1升级到版本2的私有方法先删除原有的笔记表TABLE.NOTE和数据dataTABLE.DATA如果存在的话
// 然后重新创建这两张表以实现数据库结构的更新可能是因为版本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);
}
// 用于将数据库从版本2升级到版本3的私有方法主要进行了以下操作
// 1. 删除一些不再使用的触发器(可能是因为业务逻辑变更,这些触发器不再需要了)。
// 2. 向笔记表TABLE.NOTE添加一个新的列gtask_id用于存储与gtask相关的ID信息可能是为了和外部任务管理等功能进行对接扩展。
// 3. 创建一个新的系统文件夹(回收站文件夹),方便对删除的笔记等数据进行统一管理和后续可能的恢复等操作
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
// 用于将数据库从版本3升级到版本4的私有方法向笔记表TABLE.NOTE添加一个新的列version用于记录笔记数据的版本信息
// 方便后续进行数据更新、兼容性处理等相关操作,例如根据版本号来判断是否需要执行某些特定的逻辑以适配不同版本的数据格式等
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}

@ -0,0 +1,419 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
// NotesProvider类继承自ContentProvider是安卓中用于实现内容提供器的核心类
// 它负责处理与笔记数据相关的各种操作查询、插入、删除、更新等并根据不同的Uri来区分操作的对象是笔记数据还是其他相关数据等
// 同时还涉及搜索功能相关的数据处理以及数据变更通知等机制,是应用内不同组件间共享笔记数据的重要桥梁。
public class NotesProvider extends ContentProvider {
// 用于匹配不同Uri的UriMatcher实例通过它可以根据传入的Uri来判断对应的操作类型和操作对象
// 在后续的各种数据操作方法如query、insert等中会频繁使用到它来进行路由判断。
private static final UriMatcher mMatcher;
// 数据库帮助类实例用于辅助进行数据库相关操作如获取数据库连接、执行SQL语句等通过它可以方便地操作应用的笔记数据库。
private NotesDatabaseHelper mHelper;
// 用于日志记录的标签,方便在代码中输出与该内容提供器相关的日志信息时进行标识区分,便于调试和查看运行情况。
private static final String TAG = "NotesProvider";
// 定义不同的常量用于表示不同类型的Uri对应的匹配码方便在UriMatcher中进行匹配和后续在代码中根据匹配结果进行不同的操作分支处理。
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;
// 静态代码块用于初始化UriMatcher实例将不同的Uri路径和对应的匹配码添加进去
// 这样后续就能根据传入的Uri准确判断出是针对笔记表的整体操作、笔记表中单个条目的操作、数据表的操作、数据表中单个条目的操作还是搜索相关的操作等情况。
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
/**
* x'0A' SQLite'\n'
* 便
* NoteColumns
* IDID
*
*/
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
// 定义一个用于搜索笔记摘要内容的查询语句字符串指定了从笔记表TABLE.NOTE中查询数据的条件
// 即根据摘要内容NoteColumns.SNIPPET进行模糊匹配LIKE操作符并且排除回收站文件夹Notes.ID_TRASH_FOLER中的笔记以及只查询类型为普通笔记Notes.TYPE_NOTE的记录
// 后续在执行搜索相关查询操作时会使用到这个语句来获取符合条件的搜索结果数据。
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
// 重写ContentProvider的onCreate方法在内容提供器创建时被调用
// 该方法通过调用NotesDatabaseHelper的单例方法获取数据库帮助类实例用于后续数据库操作
// 如果获取成功则返回true表示内容提供器初始化成功。
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
// 重写ContentProvider的query方法用于处理数据查询操作根据传入的不同Uri来执行相应的数据库查询逻辑
// 并返回查询结果的游标Cursor不同的Uri匹配情况对应不同的查询表、查询条件以及处理方式等。
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
// 获取可读的数据库连接对象,用于执行查询操作,不会对数据库进行写操作,保证数据的安全性。
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
// 通过UriMatcher匹配传入的Uri根据匹配结果进入不同的分支处理查询逻辑。
switch (mMatcher.match(uri)) {
// 如果匹配到的是笔记表TABLE.NOTE的整体查询Uri对应URI_NOTE常量
// 则直接使用数据库连接对象执行查询操作,传入表名、投影字段(可指定要查询的列)、查询条件、查询参数以及排序方式等参数进行查询。
case URI_NOTE:
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
// 如果匹配到的是笔记表TABLE.NOTE中单个条目的查询Uri对应URI_NOTE_ITEM常量
// 先从Uri中获取该条目的ID通过getPathSegments方法获取路径片段并取第二个元素因为路径格式类似 "note/[id]"
// 然后在查询条件中添加该ID的匹配条件NoteColumns.ID + "=" + id再结合传入的其他查询条件通过parseSelection方法进一步处理进行查询。
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
// 如果匹配到的是数据表TABLE.DATA的整体查询Uri对应URI_DATA常量
// 则使用数据库连接对象对数据表执行查询操作,传入相应的查询参数进行查询,与笔记表整体查询类似,只是操作的表不同。
case URI_DATA:
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
// 如果匹配到的是数据表TABLE.DATA中单个条目的查询Uri对应URI_DATA_ITEM常量
// 同样先从Uri中获取该条目的ID然后在查询条件中添加该ID的匹配条件再结合传入的其他查询条件进行查询操作与笔记表单个条目查询类似只是针对的数据表不同。
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
// 如果匹配到的是搜索相关的Uri对应URI_SEARCH或URI_SEARCH_SUGGEST常量
// 则需要进行特殊的搜索相关的查询处理,有以下步骤:
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
// 如果传入了排序方式sortOrder或者指定了投影字段projection等参数
// 则抛出异常,因为对于搜索相关的查询不允许这样指定这些参数(按照此处的设计逻辑)。
if (sortOrder!= null || projection!= null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null;
// 如果匹配的是搜索建议URI_SEARCH_SUGGEST的Uri并且Uri路径片段数量大于1说明有具体的搜索关键词参数
// 则获取路径片段中的第二个元素作为搜索关键词。
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
// 如果是普通搜索URI_SEARCH的Uri则从Uri的查询参数中获取名为"pattern"的参数作为搜索关键词。
searchString = uri.getQueryParameter("pattern");
}
// 如果搜索关键词为空即没有指定搜索内容则直接返回null表示没有查询结果。
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
// 将搜索关键词进行格式化,添加通配符(%)使其能进行模糊匹配查询,例如将关键词"abc"格式化为"%abc%"。
searchString = String.format("%%%s%%", searchString);
// 使用数据库连接对象执行原生的SQL查询语句NOTES_SNIPPET_SEARCH_QUERY传入格式化后的搜索关键词作为参数获取搜索结果游标。
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[]{searchString});
} catch (IllegalStateException ex) {
// 如果在执行查询过程中出现异常,记录错误日志,方便后续排查问题。
Log.e(TAG, "got exception: " + ex.toString());
}
break;
default:
// 如果传入的Uri不匹配任何已知的情况则抛出异常表示未知的Uri说明传入的请求不符合该内容提供器的预期。
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 如果查询到的游标Cursor不为空设置游标对应的内容变更通知Uri
// 这样当该Uri对应的数据发生变化时监听该Uri的组件如ContentObserver能够收到通知并做出相应处理保持数据展示的一致性。
if (c!= null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
// 重写ContentProvider的insert方法用于处理数据插入操作根据传入的不同Uri将数据插入到相应的表中
// 并返回插入后数据对应的Uri包含新插入数据的ID等信息同时在插入成功后会发送数据变更通知告知相关组件数据已更新。
@Override
public Uri insert(Uri uri, ContentValues values) {
// 获取可写的数据库连接对象,用于执行插入操作,因为要对数据库进行写操作(插入新数据)。
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
// 通过UriMatcher匹配传入的Uri根据匹配结果进入不同的分支处理插入逻辑。
switch (mMatcher.match(uri)) {
// 如果匹配到的是笔记表TABLE.NOTE的插入Uri对应URI_NOTE常量
// 则直接使用数据库连接对象向笔记表插入数据获取插入后数据的行IDnoteId同时也将其赋值给insertedId用于后续返回插入后的Uri
case URI_NOTE:
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
// 如果匹配到的是数据表TABLE.DATA的插入Uri对应URI_DATA常量
// 先判断插入的数据中是否包含所属笔记的IDDataColumns.NOTE_ID如果包含则获取该笔记IDnoteId
// 然后使用数据库连接对象向数据表插入数据获取插入后数据的行IDdataId同样赋值给insertedId用于返回插入后的Uri
// 如果不包含所属笔记ID则记录一条调试日志表示数据格式错误缺少必要的笔记ID信息
case URI_DATA:
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
// 如果传入的Uri不匹配任何已知的插入情况则抛出异常表示未知的插入Uri说明插入请求不符合该内容提供器的预期。
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 如果插入的是笔记表数据且插入成功noteId大于0则发送笔记表对应UriNotes.CONTENT_NOTE_URI的数据变更通知
// 告知监听该Uri的组件如使用该内容提供器查询笔记表数据的Activity等数据已更新以便它们做出相应的刷新等操作。
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// 如果插入的是数据表数据且插入成功dataId大于0则发送数据表对应UriNotes.CONTENT_DATA_URI的数据变更通知
// 告知相关组件数据表数据已更新,作用与笔记表数据变更通知类似,保持数据展示和使用的一致性。
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
// 返回插入后数据对应的Uri通过ContentUris工具类将原始的插入Uri和新插入数据的行ID进行拼接形成完整的包含新数据ID信息的Uri方便后续使用如查询、更新等操作可基于此Uri
return ContentUris.withAppendedId(uri, insertedId);
}
// 重写ContentProvider的delete方法用于处理数据删除操作根据传入的不同Uri从相应的表中删除符合条件的数据
// 并返回删除的记录行数,同时在删除成功且满足一定条件时会发送数据变更通知,告知相关组件数据已更新。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
// 获取可写的数据库连接对象,用于执行删除操作,因为要对数据库进行写操作(删除数据)。
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 通过UriMatcher匹配传入的Uri根据匹配结果进入不同的分支处理删除逻辑。
// 重写ContentProvider的delete方法用于处理数据删除操作根据传入的不同Uri从相应的表中删除符合条件的数据
// 并返回删除的记录行数,同时在删除成功且满足一定条件时会发送数据变更通知,告知相关组件数据已更新。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
// 获取可写的数据库连接对象,用于执行删除操作,因为要对数据库进行写操作(删除数据)。
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 通过UriMatcher匹配传入的Uri根据匹配结果进入不同的分支处理删除逻辑。
switch (mMatcher.match(uri)) {
// 如果匹配到的是笔记表TABLE.NOTE的整体删除Uri对应URI_NOTE常量
// 对传入的查询条件selection进行补充添加笔记ID大于0的限制条件因为系统文件夹等特殊记录ID可能小于等于0不允许删除
// 然后使用数据库连接对象执行删除操作,传入笔记表名、处理后的查询条件以及查询参数,获取删除的记录行数。
case URI_NOTE:
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
// 如果匹配到的是笔记表TABLE.NOTE中单个条目的删除Uri对应URI_NOTE_ITEM常量
// 先从Uri中获取该条目的ID通过getPathSegments方法获取路径片段并取第二个元素因为路径格式类似 "note/[id]"
// 然后将获取的ID转换为长整型Long.valueOf判断如果该ID小于等于0说明是系统文件夹等不允许删除的记录直接跳出本次删除操作
// 若ID大于0则在查询条件中添加该ID的精确匹配条件NoteColumns.ID + "=" + id再结合传入的其他查询条件通过parseSelection方法进一步处理进行删除操作获取删除的记录行数。
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
*/
long noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
}
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
// 如果匹配到的是数据表TABLE.DATA的整体删除Uri对应URI_DATA常量
// 直接使用数据库连接对象执行删除操作传入数据表名、查询条件以及查询参数获取删除的记录行数同时标记deleteData为true表示此次删除操作涉及的数据表是数据data表。
case URI_DATA:
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true;
break;
// 如果匹配到的是数据表TABLE.DATA中单个条目的删除Uri对应URI_DATA_ITEM常量
// 同样先从Uri中获取该条目的ID然后在查询条件中添加该ID的精确匹配条件再结合传入的其他查询条件进行删除操作获取删除的记录行数也标记deleteData为true。
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
// 如果传入的Uri不匹配任何已知的删除情况则抛出异常表示未知的删除Uri说明删除请求不符合该内容提供器的预期。
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 如果删除的记录行数大于0表示删除操作成功此时根据是否涉及数据data表的删除deleteData标记来决定发送哪些数据变更通知。
if (count > 0) {
// 如果涉及数据data表的删除deleteData为true则发送笔记表对应UriNotes.CONTENT_NOTE_URI的数据变更通知
// 因为数据data表中的数据与笔记表中的笔记存在关联数据删除可能影响到笔记相关展示等情况告知监听该Uri的组件如使用该内容提供器查询笔记表数据的Activity等数据已更新以便它们做出相应的刷新等操作。
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 无论是否涉及数据data表的删除都发送当前操作的Uri对应的数据变更通知告知相关组件该Uri对应的数据已更新保持数据展示和使用的一致性。
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
// 重写ContentProvider的update方法用于处理数据更新操作根据传入的不同Uri更新相应表中符合条件的数据
// 并返回更新的记录行数,同时在更新成功且满足一定条件时会发送数据变更通知,告知相关组件数据已更新。
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 通过UriMatcher匹配传入的Uri根据匹配结果进入不同的分支处理更新逻辑。
switch (mMatcher.match(uri)) {
// 如果匹配到的是笔记表TABLE.NOTE的整体更新Uri对应URI_NOTE常量
// 先调用increaseNoteVersion方法来增加笔记的版本号可能用于记录笔记数据的更新情况便于后续处理如同步等逻辑
// 然后使用数据库连接对象执行更新操作传入笔记表名、要更新的值ContentValues、查询条件以及查询参数获取更新的记录行数。
case URI_NOTE:
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
// 如果匹配到的是笔记表TABLE.NOTE中单个条目的更新Uri对应URI_NOTE_ITEM常量
// 先从Uri中获取该条目的ID通过getPathSegments方法获取路径片段并取第二个元素因为路径格式类似 "note/[id]"
// 然后调用increaseNoteVersion方法传入该ID来增加对应笔记的版本号
// 接着在更新条件中添加该ID的精确匹配条件NoteColumns.ID + "=" + id再结合传入的其他查询条件通过parseSelection方法进一步处理进行更新操作获取更新的记录行数。
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
// 如果匹配到的是数据表TABLE.DATA的整体更新Uri对应URI_DATA常量
// 直接使用数据库连接对象执行更新操作传入数据表名、要更新的值、查询条件以及查询参数获取更新的记录行数同时标记updateData为true表示此次更新操作涉及的数据表是数据data表。
case URI_DATA:
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
// 如果匹配到的是数据表TABLE.DATA中单个条目的更新Uri对应URI_DATA_ITEM常量
// 同样先从Uri中获取该条目的ID然后在更新条件中添加该ID的精确匹配条件再结合传入的其他查询条件进行更新操作获取更新的记录行数也标记updateData为true。
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
// 如果传入的Uri不匹配任何已知的更新情况则抛出异常表示未知的更新Uri说明更新请求不符合该内容提供器的预期。
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 如果更新的记录行数大于0表示更新操作成功此时根据是否涉及数据data表的更新updateData标记来决定发送哪些数据变更通知。
if (count > 0) {
// 如果涉及数据data表的更新updateData为true则发送笔记表对应UriNotes.CONTENT_NOTE_URI的数据变更通知
// 因为数据data表中的数据与笔记表中的笔记存在关联数据更新可能影响到笔记相关展示等情况告知监听该Uri的组件如使用该内容提供器查询笔记表数据的Activity等数据已更新以便它们做出相应的刷新等操作。
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
// 无论是否涉及数据data表的更新都发送当前操作的Uri对应的数据变更通知告知相关组件该Uri对应的数据已更新保持数据展示和使用的一致性。
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
// 私有方法用于辅助处理查询条件selection如果传入的查询条件不为空字符串则在其前后添加 " AND (" 和 ")"
// 以便在后续数据库操作(如删除、更新等)中能正确拼接多个查询条件(将其作为一个整体的额外条件与其他条件进行逻辑与操作),
// 如果查询条件为空字符串,则返回空字符串,表示没有额外要添加的条件部分。
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection)? " AND (" + selection + ')' : "");
}
// 私有方法用于增加笔记的版本号NoteColumns.VERSION通过构建并执行一条SQL的UPDATE语句来实现。
// 根据传入的笔记IDid以及查询条件selection和查询参数selectionArgs来确定要更新的笔记记录范围
// 如果传入的笔记ID大于0或者查询条件不为空则添加WHERE子句来限定更新的具体记录在WHERE子句中会根据情况拼接笔记ID的匹配条件以及处理后的查询条件替换查询参数中的占位符等
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
if (id > 0 ||!TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
if (id > 0) {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
if (!TextUtils.isEmpty(selection)) {
String selectString = id > 0? parseSelection(selection) : selection;
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
sql.append(selectString);
}
mHelper.getWritableDatabase().execSQL(sql.toString());
}
// 重写ContentProvider的getType方法按照ContentProvider的规范该方法应该返回给定Uri对应的MIME类型数据
// 但此处代码中暂时没有实现具体逻辑返回null可能需要根据实际需求补充代码来正确返回对应Uri的MIME类型
// 例如对于不同的表操作笔记表、数据表等以及不同的操作类型单个条目、整体操作等返回相应准确的MIME类型以便其他组件正确识别和处理数据内容。
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}

@ -0,0 +1,82 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
public class MetaData extends Task {
private final static String TAG = MetaData.class.getSimpleName();
private String mRelatedGid = null;
public void setMeta(String gid, JSONObject metaInfo) {
try {
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid");
}
setNotes(metaInfo.toString());
setName(GTaskStringUtils.META_NOTE_NAME);
}
public String getRelatedGid() {
return mRelatedGid;
}
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
@Override
public void setContentByRemoteJSON(JSONObject js) {
super.setContentByRemoteJSON(js);
if (getNotes() != null) {
try {
JSONObject metaInfo = new JSONObject(getNotes().trim());
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
}
@Override
public void setContentByLocalJSON(JSONObject js) {
// this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
@Override
public JSONObject getLocalJSONFromContent() {
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
@Override
public int getSyncAction(Cursor c) {
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -0,0 +1,101 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import org.json.JSONObject;
public abstract class Node {
public static final int SYNC_ACTION_NONE = 0;
public static final int SYNC_ACTION_ADD_REMOTE = 1;
public static final int SYNC_ACTION_ADD_LOCAL = 2;
public static final int SYNC_ACTION_DEL_REMOTE = 3;
public static final int SYNC_ACTION_DEL_LOCAL = 4;
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
public static final int SYNC_ACTION_ERROR = 8;
private String mGid;
private String mName;
private long mLastModified;
private boolean mDeleted;
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
public abstract JSONObject getCreateAction(int actionId);
public abstract JSONObject getUpdateAction(int actionId);
public abstract void setContentByRemoteJSON(JSONObject js);
public abstract void setContentByLocalJSON(JSONObject js);
public abstract JSONObject getLocalJSONFromContent();
public abstract int getSyncAction(Cursor c);
public void setGid(String gid) {
this.mGid = gid;
}
public void setName(String name) {
this.mName = name;
}
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
public String getGid() {
return this.mGid;
}
public String getName() {
return this.mName;
}
public long getLastModified() {
return this.mLastModified;
}
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -0,0 +1,189 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.gtask.exception.ActionFailureException;
import org.json.JSONException;
import org.json.JSONObject;
public class SqlData {
private static final String TAG = SqlData.class.getSimpleName();
private static final int INVALID_ID = -99999;
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
DataColumns.DATA3
};
public static final int DATA_ID_COLUMN = 0;
public static final int DATA_MIME_TYPE_COLUMN = 1;
public static final int DATA_CONTENT_COLUMN = 2;
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
private ContentResolver mContentResolver;
private boolean mIsCreate;
private long mDataId;
private String mDataMimeType;
private String mDataContent;
private long mDataContentData1;
private String mDataContentData3;
private ContentValues mDiffDataValues;
public SqlData(Context context) {
mContentResolver = context.getContentResolver();
mIsCreate = true;
mDataId = INVALID_ID;
mDataMimeType = DataConstants.NOTE;
mDataContent = "";
mDataContentData1 = 0;
mDataContentData3 = "";
mDiffDataValues = new ContentValues();
}
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDiffDataValues = new ContentValues();
}
private void loadFromCursor(Cursor c) {
mDataId = c.getLong(DATA_ID_COLUMN);
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
mDataContent = c.getString(DATA_CONTENT_COLUMN);
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
public void setContent(JSONObject js) throws JSONException {
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
mDataId = dataId;
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
mDataMimeType = dataMimeType;
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
mDataContent = dataContent;
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
mDataContentData1 = dataContentData1;
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
mDataContentData3 = dataContentData3;
}
public JSONObject getContent() throws JSONException {
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject js = new JSONObject();
js.put(DataColumns.ID, mDataId);
js.put(DataColumns.MIME_TYPE, mDataMimeType);
js.put(DataColumns.CONTENT, mDataContent);
js.put(DataColumns.DATA1, mDataContentData1);
js.put(DataColumns.DATA3, mDataContentData3);
return js;
}
public void commit(long noteId, boolean validateVersion, long version) {
if (mIsCreate) {
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
if (mDiffDataValues.size() > 0) {
int result = 0;
if (!validateVersion) {
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null);
} else {
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues,
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
String.valueOf(noteId), String.valueOf(version)
});
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
mDiffDataValues.clear();
mIsCreate = false;
}
public long getId() {
return mDataId;
}
}

@ -0,0 +1,505 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import net.micode.notes.tool.ResourceParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class SqlNote {
private static final String TAG = SqlNote.class.getSimpleName();
private static final int INVALID_ID = -99999;
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE,
NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID,
NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID,
NoteColumns.VERSION
};
public static final int ID_COLUMN = 0;
public static final int ALERTED_DATE_COLUMN = 1;
public static final int BG_COLOR_ID_COLUMN = 2;
public static final int CREATED_DATE_COLUMN = 3;
public static final int HAS_ATTACHMENT_COLUMN = 4;
public static final int MODIFIED_DATE_COLUMN = 5;
public static final int NOTES_COUNT_COLUMN = 6;
public static final int PARENT_ID_COLUMN = 7;
public static final int SNIPPET_COLUMN = 8;
public static final int TYPE_COLUMN = 9;
public static final int WIDGET_ID_COLUMN = 10;
public static final int WIDGET_TYPE_COLUMN = 11;
public static final int SYNC_ID_COLUMN = 12;
public static final int LOCAL_MODIFIED_COLUMN = 13;
public static final int ORIGIN_PARENT_ID_COLUMN = 14;
public static final int GTASK_ID_COLUMN = 15;
public static final int VERSION_COLUMN = 16;
private Context mContext;
private ContentResolver mContentResolver;
private boolean mIsCreate;
private long mId;
private long mAlertDate;
private int mBgColorId;
private long mCreatedDate;
private int mHasAttachment;
private long mModifiedDate;
private long mParentId;
private String mSnippet;
private int mType;
private int mWidgetId;
private int mWidgetType;
private long mOriginParent;
private long mVersion;
private ContentValues mDiffNoteValues;
private ArrayList<SqlData> mDataList;
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = true;
mId = INVALID_ID;
mAlertDate = 0;
mBgColorId = ResourceParser.getDefaultBgId(context);
mCreatedDate = System.currentTimeMillis();
mHasAttachment = 0;
mModifiedDate = System.currentTimeMillis();
mParentId = 0;
mSnippet = "";
mType = Notes.TYPE_NOTE;
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
mOriginParent = 0;
mVersion = 0;
mDiffNoteValues = new ContentValues();
mDataList = new ArrayList<SqlData>();
}
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(id);
mDataList = new ArrayList<SqlData>();
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues = new ContentValues();
}
private void loadFromCursor(long id) {
Cursor c = null;
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext();
loadFromCursor(c);
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
if (c != null)
c.close();
}
}
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
mParentId = c.getLong(PARENT_ID_COLUMN);
mSnippet = c.getString(SNIPPET_COLUMN);
mType = c.getInt(TYPE_COLUMN);
mWidgetId = c.getInt(WIDGET_ID_COLUMN);
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
mVersion = c.getLong(VERSION_COLUMN);
}
private void loadDataContent() {
Cursor c = null;
mDataList.clear();
try {
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] {
String.valueOf(mId)
}, null);
if (c != null) {
if (c.getCount() == 0) {
Log.w(TAG, "it seems that the note has not data");
return;
}
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
} else {
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
if (c != null)
c.close();
}
}
public boolean setContent(JSONObject js) {
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// for folder we can only update the snnipet and type
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
long id = note.has(NoteColumns.ID) ? note.getLong(NoteColumns.ID) : INVALID_ID;
if (mIsCreate || mId != id) {
mDiffNoteValues.put(NoteColumns.ID, id);
}
mId = id;
long alertDate = note.has(NoteColumns.ALERTED_DATE) ? note
.getLong(NoteColumns.ALERTED_DATE) : 0;
if (mIsCreate || mAlertDate != alertDate) {
mDiffNoteValues.put(NoteColumns.ALERTED_DATE, alertDate);
}
mAlertDate = alertDate;
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
mDiffNoteValues.put(NoteColumns.BG_COLOR_ID, bgColorId);
}
mBgColorId = bgColorId;
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
mDiffNoteValues.put(NoteColumns.CREATED_DATE, createDate);
}
mCreatedDate = createDate;
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
mDiffNoteValues.put(NoteColumns.HAS_ATTACHMENT, hasAttachment);
}
mHasAttachment = hasAttachment;
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
mDiffNoteValues.put(NoteColumns.MODIFIED_DATE, modifiedDate);
}
mModifiedDate = modifiedDate;
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
mDiffNoteValues.put(NoteColumns.PARENT_ID, parentId);
}
mParentId = parentId;
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
mDiffNoteValues.put(NoteColumns.WIDGET_ID, widgetId);
}
mWidgetId = widgetId;
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
mDiffNoteValues.put(NoteColumns.WIDGET_TYPE, widgetType);
}
mWidgetType = widgetType;
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
mDiffNoteValues.put(NoteColumns.ORIGIN_PARENT_ID, originParent);
}
mOriginParent = originParent;
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
sqlData = temp;
}
}
}
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
sqlData.setContent(data);
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
return true;
}
public JSONObject getContent() {
try {
JSONObject js = new JSONObject();
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject note = new JSONObject();
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
note.put(NoteColumns.BG_COLOR_ID, mBgColorId);
note.put(NoteColumns.CREATED_DATE, mCreatedDate);
note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment);
note.put(NoteColumns.MODIFIED_DATE, mModifiedDate);
note.put(NoteColumns.PARENT_ID, mParentId);
note.put(NoteColumns.SNIPPET, mSnippet);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
} else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return null;
}
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
public long getId() {
return mId;
}
public long getParentId() {
return mParentId;
}
public String getSnippet() {
return mSnippet;
}
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
public void commit(boolean validateVersion) {
if (mIsCreate) {
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
} else {
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
if (mDiffNoteValues.size() > 0) {
mVersion ++;
int result = 0;
if (!validateVersion) {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?)", new String[] {
String.valueOf(mId)
});
} else {
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "("
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
}
}
}
// refresh local info
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
mDiffNoteValues.clear();
mIsCreate = false;
}
}

@ -0,0 +1,351 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
private boolean mCompleted;
private String mNotes;
private JSONObject mMetaInfo;
private Task mPriorSibling;
private TaskList mParent;
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
String name = getName();
try {
if (mMetaInfo == null) {
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
JSONObject js = new JSONObject();
JSONObject note = new JSONObject();
JSONArray dataArray = new JSONArray();
JSONObject data = new JSONObject();
data.put(DataColumns.CONTENT, name);
dataArray.put(data);
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
return js;
} else {
// synced task
JSONObject note = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = mMetaInfo.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
public int getSyncAction(Cursor c) {
try {
JSONObject noteInfo = null;
if (mMetaInfo != null && mMetaInfo.has(GTaskStringUtils.META_HEAD_NOTE)) {
noteInfo = mMetaInfo.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
}
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// validate the note id now
if (c.getLong(SqlNote.ID_COLUMN) != noteInfo.getLong(NoteColumns.ID)) {
Log.w(TAG, "note id doesn't match");
return SYNC_ACTION_UPDATE_LOCAL;
}
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
return SYNC_ACTION_UPDATE_CONFLICT;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
public void setNotes(String notes) {
this.mNotes = notes;
}
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
public void setParent(TaskList parent) {
this.mParent = parent;
}
public boolean getCompleted() {
return this.mCompleted;
}
public String getNotes() {
return this.mNotes;
}
public Task getPriorSibling() {
return this.mPriorSibling;
}
public TaskList getParent() {
return this.mParent;
}
}

@ -0,0 +1,343 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public class TaskList extends Node {
private static final String TAG = TaskList.class.getSimpleName();
private int mIndex;
private ArrayList<Task> mChildren;
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js;
}
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js;
}
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
}
}
}
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
} else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
else
Log.e(TAG, "invalid system folder");
} else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
public JSONObject getLocalJSONFromContent() {
try {
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject();
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
folder.put(NoteColumns.SNIPPET, folderName);
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
public int getSyncAction(Cursor c) {
try {
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// for folder conflicts, just apply local modification
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
public int getChildTaskCount() {
return mChildren.size();
}
public boolean addChildTask(Task task) {
boolean ret = false;
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(task);
if (ret) {
// need to set prior sibling and parent
task.setPriorSibling(mChildren.isEmpty() ? null : mChildren
.get(mChildren.size() - 1));
task.setParent(this);
}
}
return ret;
}
public boolean addChildTask(Task task, int index) {
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
mChildren.add(index, task);
// update the task list
Task preTask = null;
Task afterTask = null;
if (index != 0)
preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
afterTask = mChildren.get(index + 1);
task.setPriorSibling(preTask);
if (afterTask != null)
afterTask.setPriorSibling(task);
}
return true;
}
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task);
if (ret) {
// reset prior sibling and parent
task.setPriorSibling(null);
task.setParent(null);
// update the task list
if (index != mChildren.size()) {
mChildren.get(index).setPriorSibling(
index == 0 ? null : mChildren.get(index - 1));
}
}
}
return ret;
}
public boolean moveChildTask(Task task, int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
if (pos == index)
return true;
return (removeChildTask(task) && addChildTask(task, index));
}
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
if (t.getGid().equals(gid)) {
return t;
}
}
return null;
}
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
}
return mChildren.get(index);
}
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
return task;
}
return null;
}
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
public void setIndex(int index) {
this.mIndex = index;
}
public int getIndex() {
return this.mIndex;
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
public ActionFailureException() {
super();
}
public ActionFailureException(String paramString) {
super(paramString);
}
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.exception;
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
public NetworkFailureException() {
super();
}
public NetworkFailureException(String paramString) {
super(paramString);
}
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -0,0 +1,143 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import net.micode.notes.R;
import net.micode.notes.ui.NotesListActivity;
import net.micode.notes.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
public interface OnCompleteListener {
void onComplete();
}
private Context mContext;
private NotificationManager mNotifiManager;
private GTaskManager mTaskManager;
private OnCompleteListener mOnCompleteListener;
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
}
public void cancelSync() {
mTaskManager.cancelSync();
}
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
// private void showNotification(int tickerId, String content) {
// Notification notification = new Notification(R.drawable.notification, mContext
// .getString(tickerId), System.currentTimeMillis());
// notification.defaults = Notification.DEFAULT_LIGHTS;
// notification.flags = Notification.FLAG_AUTO_CANCEL;
// PendingIntent pendingIntent;
// if (tickerId != R.string.ticker_success) {
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesPreferenceActivity.class), 0);
//
// } else {
// pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
// NotesListActivity.class), 0);
// }
// notification.setLatestEventInfo(mContext, mContext.getString(R.string.app_name), content,
// pendingIntent);
// mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
// }
private void showNotification(int tickerId, String content) {
PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
Notification.Builder builder = new Notification.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setOngoing(true);
Notification notification=builder.getNotification();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
return mTaskManager.sync(mContext, this);
}
@Override
protected void onProgressUpdate(String... progress) {
showNotification(R.string.ticker_syncing, progress[0]);
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
@Override
protected void onPostExecute(Integer result) {
if (result == GTaskManager.STATE_SUCCESS) {
showNotification(R.string.ticker_success, mContext.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
NotesPreferenceActivity.setLastSyncTime(mContext, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
showNotification(R.string.ticker_fail, mContext.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
showNotification(R.string.ticker_cancel, mContext
.getString(R.string.error_sync_cancelled));
}
if (mOnCompleteListener != null) {
new Thread(new Runnable() {
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
}

@ -0,0 +1,585 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.gtask.data.Node;
import net.micode.notes.gtask.data.Task;
import net.micode.notes.gtask.data.TaskList;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.gtask.exception.NetworkFailureException;
import net.micode.notes.tool.GTaskStringUtils;
import net.micode.notes.ui.NotesPreferenceActivity;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public class GTaskClient {
private static final String TAG = GTaskClient.class.getSimpleName();
private static final String GTASK_URL = "https://mail.google.com/tasks/";
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
private static GTaskClient mInstance = null;
private DefaultHttpClient mHttpClient;
private String mGetUrl;
private String mPostUrl;
private long mClientVersion;
private boolean mLoggedin;
private long mLastLoginTime;
private int mActionId;
private Account mAccount;
private JSONArray mUpdateArray;
private GTaskClient() {
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
public boolean login(Activity activity) {
// we suppose that the cookie would expire after 5 minutes
// then we need to re-login
final long interval = 1000 * 60 * 5;
if (mLastLoginTime + interval < System.currentTimeMillis()) {
mLoggedin = false;
}
// need to re-login after account switch
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
.getSyncAccountName(activity))) {
mLoggedin = false;
}
if (mLoggedin) {
Log.d(TAG, "already logged in");
return true;
}
mLastLoginTime = System.currentTimeMillis();
String authToken = loginGoogleAccount(activity, false);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
// login with custom domain if necessary
if (!(mAccount.name.toLowerCase().endsWith("gmail.com") || mAccount.name.toLowerCase()
.endsWith("googlemail.com"))) {
StringBuilder url = new StringBuilder(GTASK_URL).append("a/");
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
mGetUrl = url.toString() + "ig";
mPostUrl = url.toString() + "r/ig";
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true;
}
}
// try to login with google official url
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
mLoggedin = true;
return true;
}
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken;
AccountManager accountManager = AccountManager.get(activity);
Account[] accounts = accountManager.getAccountsByType("com.google");
if (accounts.length == 0) {
Log.e(TAG, "there is no available google account");
return null;
}
String accountName = NotesPreferenceActivity.getSyncAccountName(activity);
Account account = null;
for (Account a : accounts) {
if (a.name.equals(accountName)) {
account = a;
break;
}
}
if (account != null) {
mAccount = account;
} else {
Log.e(TAG, "unable to get an account with the same name in the settings");
return null;
}
// get the token now
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthToken(account,
"goanna_mobile", null, activity, null, null);
try {
Bundle authTokenBundle = accountManagerFuture.getResult();
authToken = authTokenBundle.getString(AccountManager.KEY_AUTHTOKEN);
if (invalidateToken) {
accountManager.invalidateAuthToken("com.google", authToken);
loginGoogleAccount(activity, false);
}
} catch (Exception e) {
Log.e(TAG, "get auth token failed");
authToken = null;
}
return authToken;
}
private boolean tryToLoginGtask(Activity activity, String authToken) {
if (!loginGtask(authToken)) {
// maybe the auth token is out of date, now let's invalidate the
// token and try again
authToken = loginGoogleAccount(activity, true);
if (authToken == null) {
Log.e(TAG, "login google account failed");
return false;
}
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");
return false;
}
}
return true;
}
private boolean loginGtask(String authToken) {
int timeoutConnection = 10000;
int timeoutSocket = 15000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
mHttpClient = new DefaultHttpClient(httpParameters);
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
mHttpClient.setCookieStore(localBasicCookieStore);
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// login gtask
try {
String loginUrl = mGetUrl + "?auth=" + authToken;
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// get the cookie now
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie");
}
// get the client version
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
// simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");
return false;
}
return true;
}
private int getActionId() {
return mActionId++;
}
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
httpPost.setHeader("AT", "1");
return httpPost;
}
private String getResponseContent(HttpEntity entity) throws IOException {
String contentEncoding = null;
if (entity.getContentEncoding() != null) {
contentEncoding = entity.getContentEncoding().getValue();
Log.d(TAG, "encoding: " + contentEncoding);
}
InputStream input = entity.getContent();
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
} else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
Inflater inflater = new Inflater(true);
input = new InflaterInputStream(entity.getContent(), inflater);
}
try {
InputStreamReader isr = new InputStreamReader(input);
BufferedReader br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
while (true) {
String buff = br.readLine();
if (buff == null) {
return sb.toString();
}
sb = sb.append(buff);
}
} finally {
input.close();
}
}
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
HttpPost httpPost = createHttpPost();
try {
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
httpPost.setEntity(entity);
// execute the post
HttpResponse response = mHttpClient.execute(httpPost);
String jsString = getResponseContent(response.getEntity());
return new JSONObject(jsString);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("unable to convert response content to jsonobject");
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("error occurs when posting request");
}
}
public void createTask(Task task) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
actionList.put(task.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create task: handing jsonobject failed");
}
}
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
actionList.put(tasklist.getCreateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// post
JSONObject jsResponse = postRequest(jsPost);
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create tasklist: handing jsonobject failed");
}
}
public void commitUpdate() throws NetworkFailureException {
if (mUpdateArray != null) {
try {
JSONObject jsPost = new JSONObject();
// action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
}
}
}
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// too many update items may result in an error
// set max to 10 items
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_MOVE);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
if (preParent == curParent && task.getPriorSibling() != null) {
// put prioring_sibing_id only if moving within the tasklist and
// it is not the first one
action.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, task.getPriorSibling());
}
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
if (preParent != curParent) {
// put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("move task: handing jsonobject failed");
}
}
public void deleteNode(Node node) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
// action_list
node.setDeleted(true);
actionList.put(node.getUpdateAction(getActionId()));
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
postRequest(jsPost);
mUpdateArray = null;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("delete node: handing jsonobject failed");
}
}
public JSONArray getTaskLists() throws NetworkFailureException {
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {
HttpGet httpGet = new HttpGet(mGetUrl);
HttpResponse response = null;
response = mHttpClient.execute(httpGet);
// get the task list
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
jsString = resString.substring(begin + jsBegin.length(), end);
}
JSONObject js = new JSONObject(jsString);
return js.getJSONObject("t").getJSONArray(GTaskStringUtils.GTASK_JSON_LISTS);
} catch (ClientProtocolException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (IOException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed");
}
}
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
commitUpdate();
try {
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray();
JSONObject action = new JSONObject();
// action_list
action.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_GETALL);
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
actionList.put(action);
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
JSONObject jsResponse = postRequest(jsPost);
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
public Account getSyncAccount() {
return mAccount;
}
public void resetUpdateArray() {
mUpdateArray = null;
}
}

@ -0,0 +1,800 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.data.MetaData;
import net.micode.notes.gtask.data.Node;
import net.micode.notes.gtask.data.SqlNote;
import net.micode.notes.gtask.data.Task;
import net.micode.notes.gtask.data.TaskList;
import net.micode.notes.gtask.exception.ActionFailureException;
import net.micode.notes.gtask.exception.NetworkFailureException;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
public class GTaskManager {
private static final String TAG = GTaskManager.class.getSimpleName();
public static final int STATE_SUCCESS = 0;
public static final int STATE_NETWORK_ERROR = 1;
public static final int STATE_INTERNAL_ERROR = 2;
public static final int STATE_SYNC_IN_PROGRESS = 3;
public static final int STATE_SYNC_CANCELLED = 4;
private static GTaskManager mInstance = null;
private Activity mActivity;
private Context mContext;
private ContentResolver mContentResolver;
private boolean mSyncing;
private boolean mCancelled;
private HashMap<String, TaskList> mGTaskListHashMap;
private HashMap<String, Node> mGTaskHashMap;
private HashMap<String, MetaData> mMetaHashMap;
private TaskList mMetaList;
private HashSet<Long> mLocalDeleteIdMap;
private HashMap<String, Long> mGidToNid;
private HashMap<Long, String> mNidToGid;
private GTaskManager() {
mSyncing = false;
mCancelled = false;
mGTaskListHashMap = new HashMap<String, TaskList>();
mGTaskHashMap = new HashMap<String, Node>();
mMetaHashMap = new HashMap<String, MetaData>();
mMetaList = null;
mLocalDeleteIdMap = new HashSet<Long>();
mGidToNid = new HashMap<String, Long>();
mNidToGid = new HashMap<Long, String>();
}
public static synchronized GTaskManager getInstance() {
if (mInstance == null) {
mInstance = new GTaskManager();
}
return mInstance;
}
public synchronized void setActivityContext(Activity activity) {
// used for getting authtoken
mActivity = activity;
}
public int sync(Context context, GTaskASyncTask asyncTask) {
if (mSyncing) {
Log.d(TAG, "Sync is in progress");
return STATE_SYNC_IN_PROGRESS;
}
mContext = context;
mContentResolver = mContext.getContentResolver();
mSyncing = true;
mCancelled = false;
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
try {
GTaskClient client = GTaskClient.getInstance();
client.resetUpdateArray();
// login google task
if (!mCancelled) {
if (!client.login(mActivity)) {
throw new NetworkFailureException("login google task failed");
}
}
// get the task list from google
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
initGTaskList();
// do content sync work
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
syncContent();
} catch (NetworkFailureException e) {
Log.e(TAG, e.toString());
return STATE_NETWORK_ERROR;
} catch (ActionFailureException e) {
Log.e(TAG, e.toString());
return STATE_INTERNAL_ERROR;
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return STATE_INTERNAL_ERROR;
} finally {
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
mSyncing = false;
}
return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
}
private void initGTaskList() throws NetworkFailureException {
if (mCancelled)
return;
GTaskClient client = GTaskClient.getInstance();
try {
JSONArray jsTaskLists = client.getTaskLists();
// init meta list first
mMetaList = null;
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name
.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
mMetaList = new TaskList();
mMetaList.setContentByRemoteJSON(object);
// load meta data
JSONArray jsMetas = client.getTaskList(gid);
for (int j = 0; j < jsMetas.length(); j++) {
object = (JSONObject) jsMetas.getJSONObject(j);
MetaData metaData = new MetaData();
metaData.setContentByRemoteJSON(object);
if (metaData.isWorthSaving()) {
mMetaList.addChildTask(metaData);
if (metaData.getGid() != null) {
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
}
}
}
// create meta list if not existed
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META);
GTaskClient.getInstance().createTaskList(mMetaList);
}
// init task list
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
&& !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META)) {
TaskList tasklist = new TaskList();
tasklist.setContentByRemoteJSON(object);
mGTaskListHashMap.put(gid, tasklist);
mGTaskHashMap.put(gid, tasklist);
// load tasks
JSONArray jsTasks = client.getTaskList(gid);
for (int j = 0; j < jsTasks.length(); j++) {
object = (JSONObject) jsTasks.getJSONObject(j);
gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
Task task = new Task();
task.setContentByRemoteJSON(object);
if (task.isWorthSaving()) {
task.setMetaInfo(mMetaHashMap.get(gid));
tasklist.addChildTask(task);
mGTaskHashMap.put(gid, task);
}
}
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("initGTaskList: handing JSONObject failed");
}
}
private void syncContent() throws NetworkFailureException {
int syncType;
Cursor c = null;
String gid;
Node node;
mLocalDeleteIdMap.clear();
if (mCancelled) {
return;
}
// for local deleted note
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, null);
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_DEL_REMOTE, node, c);
}
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
}
} else {
Log.w(TAG, "failed to query trash folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// sync folder first
syncFolder();
// for note existing in database
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c);
} else {
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
// local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
// remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
doContentSync(syncType, node, c);
}
} else {
Log.w(TAG, "failed to query existing note in database");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// go through remaining items
Iterator<Map.Entry<String, Node>> iter = mGTaskHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, Node> entry = iter.next();
node = entry.getValue();
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
// mCancelled can be set by another thread, so we neet to check one by
// one
// clear local delete table
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
throw new ActionFailureException("failed to batch-delete local deleted notes");
}
}
// refresh local sync id
if (!mCancelled) {
GTaskClient.getInstance().commitUpdate();
refreshLocalSyncId();
}
}
private void syncFolder() throws NetworkFailureException {
Cursor c = null;
String gid;
Node node;
int syncType;
if (mCancelled) {
return;
}
// for root folder
try {
c = mContentResolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
Notes.ID_ROOT_FOLDER), SqlNote.PROJECTION_NOTE, null, null, null);
if (c != null) {
c.moveToNext();
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_ROOT_FOLDER);
mNidToGid.put((long) Notes.ID_ROOT_FOLDER, gid);
// for system folder, only update remote name if necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
} else {
Log.w(TAG, "failed to query root folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for call-note folder
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(Notes.ID_CALL_RECORD_FOLDER)
}, null);
if (c != null) {
if (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, (long) Notes.ID_CALL_RECORD_FOLDER);
mNidToGid.put((long) Notes.ID_CALL_RECORD_FOLDER, gid);
// for system folder, only update remote name if
// necessary
if (!node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE))
doContentSync(Node.SYNC_ACTION_UPDATE_REMOTE, node, c);
} else {
doContentSync(Node.SYNC_ACTION_ADD_REMOTE, node, c);
}
}
} else {
Log.w(TAG, "failed to query call note folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for local existing folders
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
gid = c.getString(SqlNote.GTASK_ID_COLUMN);
node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
mGidToNid.put(gid, c.getLong(SqlNote.ID_COLUMN));
mNidToGid.put(c.getLong(SqlNote.ID_COLUMN), gid);
syncType = node.getSyncAction(c);
} else {
if (c.getString(SqlNote.GTASK_ID_COLUMN).trim().length() == 0) {
// local add
syncType = Node.SYNC_ACTION_ADD_REMOTE;
} else {
// remote delete
syncType = Node.SYNC_ACTION_DEL_LOCAL;
}
}
doContentSync(syncType, node, c);
}
} else {
Log.w(TAG, "failed to query existing folder");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
// for remote add folders
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> entry = iter.next();
gid = entry.getKey();
node = entry.getValue();
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid);
doContentSync(Node.SYNC_ACTION_ADD_LOCAL, node, null);
}
}
if (!mCancelled)
GTaskClient.getInstance().commitUpdate();
}
private void doContentSync(int syncType, Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
MetaData meta;
switch (syncType) {
case Node.SYNC_ACTION_ADD_LOCAL:
addLocalNode(node);
break;
case Node.SYNC_ACTION_ADD_REMOTE:
addRemoteNode(node, c);
break;
case Node.SYNC_ACTION_DEL_LOCAL:
meta = mMetaHashMap.get(c.getString(SqlNote.GTASK_ID_COLUMN));
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
mLocalDeleteIdMap.add(c.getLong(SqlNote.ID_COLUMN));
break;
case Node.SYNC_ACTION_DEL_REMOTE:
meta = mMetaHashMap.get(node.getGid());
if (meta != null) {
GTaskClient.getInstance().deleteNode(meta);
}
GTaskClient.getInstance().deleteNode(node);
break;
case Node.SYNC_ACTION_UPDATE_LOCAL:
updateLocalNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_REMOTE:
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_UPDATE_CONFLICT:
// merging both modifications maybe a good idea
// right now just use local update simply
updateRemoteNode(node, c);
break;
case Node.SYNC_ACTION_NONE:
break;
case Node.SYNC_ACTION_ERROR:
default:
throw new ActionFailureException("unkown sync action type");
}
}
private void addLocalNode(Node node) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
if (node instanceof TaskList) {
if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT)) {
sqlNote = new SqlNote(mContext, Notes.ID_ROOT_FOLDER);
} else if (node.getName().equals(
GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_CALL_NOTE)) {
sqlNote = new SqlNote(mContext, Notes.ID_CALL_RECORD_FOLDER);
} else {
sqlNote = new SqlNote(mContext);
sqlNote.setContent(node.getLocalJSONFromContent());
sqlNote.setParentId(Notes.ID_ROOT_FOLDER);
}
} else {
sqlNote = new SqlNote(mContext);
JSONObject js = node.getLocalJSONFromContent();
try {
if (js.has(GTaskStringUtils.META_HEAD_NOTE)) {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (note.has(NoteColumns.ID)) {
long id = note.getLong(NoteColumns.ID);
if (DataUtils.existInNoteDatabase(mContentResolver, id)) {
// the id is not available, have to create a new one
note.remove(NoteColumns.ID);
}
}
}
if (js.has(GTaskStringUtils.META_HEAD_DATA)) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
if (DataUtils.existInDataDatabase(mContentResolver, dataId)) {
// the data id is not available, have to create
// a new one
data.remove(DataColumns.ID);
}
}
}
}
} catch (JSONException e) {
Log.w(TAG, e.toString());
e.printStackTrace();
}
sqlNote.setContent(js);
Long parentId = mGidToNid.get(((Task) node).getParent().getGid());
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot add local node");
}
sqlNote.setParentId(parentId.longValue());
}
// create the local node
sqlNote.setGtaskId(node.getGid());
sqlNote.commit(false);
// update gid-nid mapping
mGidToNid.put(node.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), node.getGid());
// update meta
updateRemoteMeta(node.getGid(), sqlNote);
}
private void updateLocalNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote;
// update the note locally
sqlNote = new SqlNote(mContext, c);
sqlNote.setContent(node.getLocalJSONFromContent());
Long parentId = (node instanceof Task) ? mGidToNid.get(((Task) node).getParent().getGid())
: new Long(Notes.ID_ROOT_FOLDER);
if (parentId == null) {
Log.e(TAG, "cannot find task's parent id locally");
throw new ActionFailureException("cannot update local node");
}
sqlNote.setParentId(parentId.longValue());
sqlNote.commit(true);
// update meta info
updateRemoteMeta(node.getGid(), sqlNote);
}
private void addRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote = new SqlNote(mContext, c);
Node n;
// update remotely
if (sqlNote.isNoteType()) {
Task task = new Task();
task.setContentByLocalJSON(sqlNote.getContent());
String parentGid = mNidToGid.get(sqlNote.getParentId());
if (parentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot add remote task");
}
mGTaskListHashMap.get(parentGid).addChildTask(task);
GTaskClient.getInstance().createTask(task);
n = (Node) task;
// add meta
updateRemoteMeta(task.getGid(), sqlNote);
} else {
TaskList tasklist = null;
// we need to skip folder if it has already existed
String folderName = GTaskStringUtils.MIUI_FOLDER_PREFFIX;
if (sqlNote.getId() == Notes.ID_ROOT_FOLDER)
folderName += GTaskStringUtils.FOLDER_DEFAULT;
else if (sqlNote.getId() == Notes.ID_CALL_RECORD_FOLDER)
folderName += GTaskStringUtils.FOLDER_CALL_NOTE;
else
folderName += sqlNote.getSnippet();
Iterator<Map.Entry<String, TaskList>> iter = mGTaskListHashMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<String, TaskList> entry = iter.next();
String gid = entry.getKey();
TaskList list = entry.getValue();
if (list.getName().equals(folderName)) {
tasklist = list;
if (mGTaskHashMap.containsKey(gid)) {
mGTaskHashMap.remove(gid);
}
break;
}
}
// no match we can add now
if (tasklist == null) {
tasklist = new TaskList();
tasklist.setContentByLocalJSON(sqlNote.getContent());
GTaskClient.getInstance().createTaskList(tasklist);
mGTaskListHashMap.put(tasklist.getGid(), tasklist);
}
n = (Node) tasklist;
}
// update local note
sqlNote.setGtaskId(n.getGid());
sqlNote.commit(false);
sqlNote.resetLocalModified();
sqlNote.commit(true);
// gid-id mapping
mGidToNid.put(n.getGid(), sqlNote.getId());
mNidToGid.put(sqlNote.getId(), n.getGid());
}
private void updateRemoteNode(Node node, Cursor c) throws NetworkFailureException {
if (mCancelled) {
return;
}
SqlNote sqlNote = new SqlNote(mContext, c);
// update remotely
node.setContentByLocalJSON(sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(node);
// update meta
updateRemoteMeta(node.getGid(), sqlNote);
// move task if necessary
if (sqlNote.isNoteType()) {
Task task = (Task) node;
TaskList preParentList = task.getParent();
String curParentGid = mNidToGid.get(sqlNote.getParentId());
if (curParentGid == null) {
Log.e(TAG, "cannot find task's parent tasklist");
throw new ActionFailureException("cannot update remote task");
}
TaskList curParentList = mGTaskListHashMap.get(curParentGid);
if (preParentList != curParentList) {
preParentList.removeChildTask(task);
curParentList.addChildTask(task);
GTaskClient.getInstance().moveTask(task, preParentList, curParentList);
}
}
// clear local modified flag
sqlNote.resetLocalModified();
sqlNote.commit(true);
}
private void updateRemoteMeta(String gid, SqlNote sqlNote) throws NetworkFailureException {
if (sqlNote != null && sqlNote.isNoteType()) {
MetaData metaData = mMetaHashMap.get(gid);
if (metaData != null) {
metaData.setMeta(gid, sqlNote.getContent());
GTaskClient.getInstance().addUpdateNode(metaData);
} else {
metaData = new MetaData();
metaData.setMeta(gid, sqlNote.getContent());
mMetaList.addChildTask(metaData);
mMetaHashMap.put(gid, metaData);
GTaskClient.getInstance().createTask(metaData);
}
}
}
private void refreshLocalSyncId() throws NetworkFailureException {
if (mCancelled) {
return;
}
// get the latest gtask list
mGTaskHashMap.clear();
mGTaskListHashMap.clear();
mMetaHashMap.clear();
initGTaskList();
Cursor c = null;
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
String gid = c.getString(SqlNote.GTASK_ID_COLUMN);
Node node = mGTaskHashMap.get(gid);
if (node != null) {
mGTaskHashMap.remove(gid);
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_ID, node.getLastModified());
mContentResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI,
c.getLong(SqlNote.ID_COLUMN)), values, null, null);
} else {
Log.e(TAG, "something is missed");
throw new ActionFailureException(
"some local items don't have gid after sync");
}
}
} else {
Log.w(TAG, "failed to query local note to refresh sync id");
}
} finally {
if (c != null) {
c.close();
c = null;
}
}
}
public String getSyncAccount() {
return GTaskClient.getInstance().getSyncAccount().name;
}
public void cancelSync() {
mCancelled = true;
}
}

@ -0,0 +1,128 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.gtask.remote;
import android.app.Activity;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
public class GTaskSyncService extends Service {
public final static String ACTION_STRING_NAME = "sync_action_type";
public final static int ACTION_START_SYNC = 0;
public final static int ACTION_CANCEL_SYNC = 1;
public final static int ACTION_INVALID = 2;
public final static String GTASK_SERVICE_BROADCAST_NAME = "net.micode.notes.gtask.remote.gtask_sync_service";
public final static String GTASK_SERVICE_BROADCAST_IS_SYNCING = "isSyncing";
public final static String GTASK_SERVICE_BROADCAST_PROGRESS_MSG = "progressMsg";
private static GTaskASyncTask mSyncTask = null;
private static String mSyncProgress = "";
private void startSync() {
if (mSyncTask == null) {
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
public void onComplete() {
mSyncTask = null;
sendBroadcast("");
stopSelf();
}
});
sendBroadcast("");
mSyncTask.execute();
}
}
private void cancelSync() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
@Override
public void onCreate() {
mSyncTask = null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
startSync();
break;
case ACTION_CANCEL_SYNC:
cancelSync();
break;
default:
break;
}
return START_STICKY;
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onLowMemory() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
public IBinder onBind(Intent intent) {
return null;
}
public void sendBroadcast(String msg) {
mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
sendBroadcast(intent);
}
public static void startSync(Activity activity) {
GTaskManager.getInstance().setActivityContext(activity);
Intent intent = new Intent(activity, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
activity.startService(intent);
}
public static void cancelSync(Context context) {
Intent intent = new Intent(context, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent);
}
public static boolean isSyncing() {
return mSyncTask != null;
}
public static String getProgressString() {
return mSyncProgress;
}
}

@ -0,0 +1,253 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import java.util.ArrayList;
public class Note {
private ContentValues mNoteDiffValues;
private NoteData mNoteData;
private static final String TAG = "Note";
/**
* Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// Create a new note in the database
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
values.put(NoteColumns.MODIFIED_DATE, createdTime);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.PARENT_ID, folderId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
long noteId = 0;
try {
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
public long getTextDataId() {
return mNoteData.mTextDataId;
}
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
if (!isLocalModified()) {
return true;
}
/**
* In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and
* {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the
* note data info
*/
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// Do not return, fall through
}
mNoteDiffValues.clear();
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
return false;
}
return true;
}
private class NoteData {
private long mTextDataId;
private ContentValues mTextDataValues;
private long mCallDataId;
private ContentValues mCallDataValues;
private static final String TAG = "NoteData";
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
}
mTextDataId = id;
}
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
}
mCallDataId = id;
}
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety
*/
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null;
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear();
}
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear();
}
if (operationList.size() > 0) {
try {
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
return null;
}
}
}

@ -0,0 +1,368 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.model;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
public class WorkingNote {
// Note for the working note
private Note mNote;
// Note Id
private long mNoteId;
// Note content
private String mContent;
// Note mode
private int mMode;
private long mAlertDate;
private long mModifiedDate;
private int mBgColorId;
private int mWidgetId;
private int mWidgetType;
private long mFolderId;
private Context mContext;
private static final String TAG = "WorkingNote";
private boolean mIsDeleted;
private NoteSettingChangedListener mNoteSettingStatusListener;
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE
};
private static final int DATA_ID_COLUMN = 0;
private static final int DATA_CONTENT_COLUMN = 1;
private static final int DATA_MIME_TYPE_COLUMN = 2;
private static final int DATA_MODE_COLUMN = 3;
private static final int NOTE_PARENT_ID_COLUMN = 0;
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
private static final int NOTE_WIDGET_ID_COLUMN = 3;
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
loadNote();
}
private void loadNote() {
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
}
cursor.close();
} else {
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData();
}
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
}, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext());
}
cursor.close();
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
return note;
}
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
mNote.syncNote(mContext, mNoteId);
/**
* Update widget content if there exist any widget of this note
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return true;
} else {
return false;
}
}
public boolean existInDatabase() {
return mNoteId > 0;
}
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
return false;
} else {
return true;
}
}
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
public String getContent() {
return mContent;
}
public long getAlertDate() {
return mAlertDate;
}
public long getModifiedDate() {
return mModifiedDate;
}
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
public int getBgColorId() {
return mBgColorId;
}
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
public int getCheckListMode() {
return mMode;
}
public long getNoteId() {
return mNoteId;
}
public long getFolderId() {
return mFolderId;
}
public int getWidgetId() {
return mWidgetId;
}
public int getWidgetType() {
return mWidgetType;
}
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
* Called when user set clock
*/
void onClockAlertChanged(long date, boolean set);
/**
* Call when user create note from widget
*/
void onWidgetChanged();
/**
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}
}

@ -0,0 +1,344 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class BackupUtils {
private static final String TAG = "BackupUtils";
// Singleton stuff
private static BackupUtils sInstance;
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new BackupUtils(context);
}
return sInstance;
}
/**
* Following states are signs to represents backup or restore
* status
*/
// Currently, the sdcard is not mounted
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// The backup file not exist
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// The data is not well formated, may be changed by other programs
public static final int STATE_DATA_DESTROIED = 2;
// Some run-time exception which causes restore or backup fails
public static final int STATE_SYSTEM_ERROR = 3;
// Backup or restore success
public static final int STATE_SUCCESS = 4;
private TextExport mTextExport;
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
public int exportToText() {
return mTextExport.exportToText();
}
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
private static class TextExport {
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
NoteColumns.SNIPPET,
NoteColumns.TYPE
};
private static final int NOTE_COLUMN_ID = 0;
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
private static final int NOTE_COLUMN_SNIPPET = 2;
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
private static final int DATA_COLUMN_CONTENT = 0;
private static final int DATA_COLUMN_MIME_TYPE = 1;
private static final int DATA_COLUMN_CALL_DATE = 2;
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
private final String [] TEXT_FORMAT;
private static final int FORMAT_FOLDER_NAME = 0;
private static final int FORMAT_NOTE_DATE = 1;
private static final int FORMAT_NOTE_CONTENT = 2;
private Context mContext;
private String mFileName;
private String mFileDirectory;
public TextExport(Context context) {
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
mFileName = "";
mFileDirectory = "";
}
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* Export the folder identified by folder id to text
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// Query notes belong to this folder
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
folderId
}, null);
if (notesCursor != null) {
if (notesCursor.moveToFirst()) {
do {
// Print note's last modified date
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
notesCursor.close();
}
}
/**
* Export note identified by id to a print stream
*/
private void exportNoteToText(String noteId, PrintStream ps) {
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {
noteId
}, null);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// Print phone number
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
phoneNumber));
}
// Print call date
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate)));
// Print call attachment location
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
location));
}
} else if (DataConstants.NOTE.equals(mimeType)) {
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
if (!TextUtils.isEmpty(content)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
content));
}
}
} while (dataCursor.moveToNext());
}
dataCursor.close();
}
// print a line separator between note
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
});
} catch (IOException e) {
Log.e(TAG, e.toString());
}
}
/**
* Note will be exported as text which is user readable
*/
public int exportToText() {
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// First export folder and its notes
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// Print folder's name
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// Export notes in root's folder
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=0", null, null);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
ps.close();
return STATE_SUCCESS;
}
/**
* Get a print stream pointed to the file {@generateExportedTextFile}
*/
private PrintStream getExportToTextPrintStream() {
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (NullPointerException e) {
e.printStackTrace();
return null;
}
return ps;
}
}
/**
* Generate the text file to store imported data
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
try {
if (!filedir.exists()) {
filedir.mkdir();
}
if (!file.exists()) {
file.createNewFile();
}
return file;
} catch (SecurityException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

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

@ -0,0 +1,113 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
public class GTaskStringUtils {
public final static String GTASK_JSON_ACTION_ID = "action_id";
public final static String GTASK_JSON_ACTION_LIST = "action_list";
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
public final static String GTASK_JSON_COMPLETED = "completed";
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
public final static String GTASK_JSON_DELETED = "deleted";
public final static String GTASK_JSON_DEST_LIST = "dest_list";
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
public final static String GTASK_JSON_ID = "id";
public final static String GTASK_JSON_INDEX = "index";
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
public final static String GTASK_JSON_LIST_ID = "list_id";
public final static String GTASK_JSON_LISTS = "lists";
public final static String GTASK_JSON_NAME = "name";
public final static String GTASK_JSON_NEW_ID = "new_id";
public final static String GTASK_JSON_NOTES = "notes";
public final static String GTASK_JSON_PARENT_ID = "parent_id";
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
public final static String GTASK_JSON_RESULTS = "results";
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
public final static String GTASK_JSON_TASKS = "tasks";
public final static String GTASK_JSON_TYPE = "type";
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
public final static String GTASK_JSON_TYPE_TASK = "TASK";
public final static String GTASK_JSON_USER = "user";
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
public final static String FOLDER_DEFAULT = "Default";
public final static String FOLDER_CALL_NOTE = "Call_Note";
public final static String FOLDER_META = "METADATA";
public final static String META_HEAD_GTASK_ID = "meta_gid";
public final static String META_HEAD_NOTE = "meta_note";
public final static String META_HEAD_DATA = "meta_data";
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -0,0 +1,181 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.preference.PreferenceManager;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
public class ResourceParser {
public static final int YELLOW = 0;
public static final int BLUE = 1;
public static final int WHITE = 2;
public static final int GREEN = 3;
public static final int RED = 4;
public static final int BG_DEFAULT_COLOR = YELLOW;
public static final int TEXT_SMALL = 0;
public static final int TEXT_MEDIUM = 1;
public static final int TEXT_LARGE = 2;
public static final int TEXT_SUPER = 3;
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
public static class NoteBgResources {
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
R.drawable.edit_white,
R.drawable.edit_green,
R.drawable.edit_red
};
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
};
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
public static int getDefaultBgId(Context context) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
return BG_DEFAULT_COLOR;
}
}
public static class NoteItemBgResources {
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
};
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
};
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
};
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
};
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
public static class WidgetBgResources {
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
};
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
};
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
public static class TextAppearanceResources {
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
R.style.TextAppearanceLarge,
R.style.TextAppearanceSuper
};
public static int getTexAppearanceResource(int id) {
/**
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE;
}
return TEXTAPPEARANCE_RESOURCES[id];
}
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}
}
}

@ -0,0 +1,197 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.Window;
import android.view.WindowManager;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.io.IOException;
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private long mNoteId; //文本在数据库存储的ID号
private String mSnippet;//闹钟提示时出现的文本片段
private static final int SNIPPET_PREW_MAX_LEN = 60;
MediaPlayer mPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Bundle 类型的数据与Map 类型的数据相似,都是以 key-value 的形式存储数据的
//onsaveInstanceState 方法是用来保存 Activity 的状态的
//能从 onCreate 的参数 savedInsanceState 中获得状态数据
requestWindowFeature(Window.FEATURE_NO_TITLE);
//界面显示--无标题
final Window win = getWindow();
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
//保持窗体点亮
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
//将窗体点亮
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
//允许窗体点亮时锁屏
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}//在手机锁屏后如果到了闹钟提示时间,点亮屏幕
Intent intent = getIntent();
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
//根据ID从数据库中获取标签的内容:
//getContentResolver()是实现数据共享,实例存储。
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
//判断标签片段是否达到符合长度
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
}
mPlayer = new MediaPlayer();
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
//弹出对话框
playAlarmSound();
//闹钟提示音激发
} else {
finish();
//完成闹钟动作
}
}
private boolean isScreenOn() {
//判断屏幕是否锁屏,调用系统函数判断,最后返回值是布尔类型
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
private void playAlarmSound() {
//闹钟提示音激发
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
//调用系统的铃声管理URI得到闹钟提示音
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
mPlayer.setDataSource(this, url);
//方法:setDataSource(Context context, Uri uri)
//解释:无返回值设置多媒体数据来源【根据Uri】
mPlayer.prepare();
//准备同步
mPlayer.setLooping(true);
//设置是否循环播放
mPlayer.start();
//开始播放
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
//e.printstackTrace()函数功能是抛出异常,还将显示出更深的调用信息
//System.out.printin(e),这个方法打印出异常,并且输出在哪里出现的异常
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
//AlertDialog的构造方法全部是Protected 的
//所以不能直接通过 new 一个 AlertDialog 来创建出一个 AlertDialo!
//要创建一个 AlertDialog就要用到AlertDialog.Builder 中的 create()方法
//如这里的 dialog 就是新建了一个 AlertDialog
dialog.setTitle(R.string.app_name);
//为对话框设置标题
dialog.setMessage(mSnippet);
//为对话框设置内容
dialog.setPositiveButton(R.string.notealert_ok, this);
//给对话框添加"Yes"按钮
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}//对话框添加"No"按钮
dialog.show().setOnDismissListener(this);
}
public void onClick(DialogInterface dialog, int which) {
switch (which) {
//用 which 来选择 click 后下一步的操作
case DialogInterface.BUTTON_NEGATIVE:
//这是取消操作
Intent intent = new Intent(this, NoteEditActivity.class);
//实现两个类间的数据传输
intent.setAction(Intent.ACTION_VIEW);
//设置动作属性
intent.putExtra(Intent.EXTRA_UID, mNoteId);
//实现 key-value 对
//EXTRA_UID为key;mNoteld 为键
startActivity(intent);
//开始动作
break;
default:
//这是确定操作
break;
}
}
public void onDismiss(DialogInterface dialog) {
//忽略
stopAlarmSound();
//停止闹钟声音
finish();
//完成该动作
}
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop();
//停止播放
mPlayer.release();
//释放 MediaPlayer 对象
mPlayer = null;
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
public class AlarmInitReceiver extends BroadcastReceiver {
private static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
//对数据库的操作调用标签ID和闹钟时间
private static final int COLUMN_ID = 0;
private static final int COLUMN_ALERTED_DATE = 1;
@Override
public void onReceive(Context context, Intent intent) {
long currentDate = System.currentTimeMillis();
//System.currentTimeMillis()产生一个当前的毫秒
//这个毫秒其实就是自1970年1月1日0时起的毫秒数
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) },
//将long 变量 currentDate 转化为字符串
null);
//Cursor 在这里的作用是通过査找数据库中的标签内容,找到和当前系统时间相等的标签
if (c != null) {
if (c.moveToFirst()) {
do {
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
Intent sender = new Intent(context, AlarmReceiver.class);
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext());
}
c.close();
}
//然而通过网上查找资料发现,对于闹钟机制的启动,通常需要上面的几个步骤
//如新建Intent、Pendinglntent 以及 AlarmManager 等
//这里就是根据数据库里的闹钟时间创建一个闹钟机制
}
}

@ -0,0 +1,33 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
intent.setClass(context, AlarmAlertActivity.class);
//启动 AlarmAlertActivity
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//activity 要存在于 activity的栈中而非activity的途径启动 activity 时必然不存在一个activity 的栈
//所以要新起一个栈装入启动的 activity
context.startActivity(intent);
}
}

@ -0,0 +1,561 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.text.DateFormatSymbols;
import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
public class DateTimePicker extends FrameLayout {
//FrameLayout 是布局模板之一
//所有的子元素全部在屏幕的右上方
private static final boolean DEFAULT_ENABLE_STATE = true;
private static final int HOURS_IN_HALF_DAY = 12;
private static final int HOURS_IN_ALL_DAY = 24;
private static final int DAYS_IN_ALL_WEEK = 7;
private static final int DATE_SPINNER_MIN_VAL = 0;
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
private static final int MINUT_SPINNER_MIN_VAL = 0;
private static final int MINUT_SPINNER_MAX_VAL = 59;
private static final int AMPM_SPINNER_MIN_VAL = 0;
private static final int AMPM_SPINNER_MAX_VAL = 1;
//初始化控件
private final NumberPicker mDateSpinner;
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
//NumberPicker是数字选择器
//这里定义的四个变量全部是在设置闹钟时需要选择的变量(如日期、时、分、上午或者下午)
private Calendar mDate;
//定义了 Calendar 类型的变量 mDate用于操作时间
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
private boolean mIsAm;
private boolean mIs24HourView;
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
private boolean mInitialising;
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
updateDateControl();
onDateTimeChanged();
}
};;//0nValueChangeListener这是时间改变监听器这里主要是对日期的监听
//将现在日期的值传递给mDate;updateDateControl是同步操作
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
//这里是对小时(Hour)的监听
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
//声明一个 Calendar 的变量 cal便于后续的操作
if (!mIs24HourView) {
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
//这里是对于 12 小时制时,晚上 11 点和12 点交替时对日期的更改
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
//这里是对于 12 小时制时凌晨11点和12点交替时对日期的更改
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl();
}//这里是对于 12 小时制时中午11点和12点交替时对 AM 和PM 的更改
} else {
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
//这里是对于 24 小时制时,晚上 11点和12 点交替时对日期的更改
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}//这里是对于 12 小时制时凌晨11点和12 点交替时对日期的更改
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
//通过数字选择器对newHour 的赋值
mDate.set(Calendar.HOUR_OF_DAY, newHour);
//通过 set 函数将新的 Hour 值传给 mDate
onDateTimeChanged();
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
//这里是对分钟(Minute)改变的监听
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0;
//设置 offset作为小时改变的一个记录数据
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
//如果原值为 59新值为0则offset 加1
//如果原值为0新值为59则offset减1
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
//对 AM 和 PM 的监听
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl();
onDateTimeChanged();
}
};
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}}//通过对数据库的访问,获取当前的系统时间
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}//上面函数的得到的是一个天文数字(1970至今的秒数)需要DateFormat 将其变得有意义
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
//获取系统时间
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
inflate(context, R.layout.datetime_picker, this);
//如果当前 Activity 里用到别的layout比如对话框layout
//还要设置这个layout上的其他组件的内容就必须用inflate()方法先将对话框的layout找出来
//然后再用findViewByld()找到它上面的其它组件
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// update controls to initial state
updateDateControl();
updateHourControl();
updateAmPmControl();
set24HourView(is24HourView);
// set to current time
setCurrentDate(date);
setEnabled(isEnabled());
// set the content descriptions
mInitialising = false;
}
//存在疑问!!!!!!!!!!!!!setEnabled 的作用
//下面的代码通过原程序的注释已经比较清晰,另外可以通过函数名来判断
//下面的各函数主要是对上面代码引用到的各函数功能的实现
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
mIsEnabled = enabled;
}
@Override
public boolean isEnabled() {
return mIsEnabled;
}
/**
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}//实现函数--得到当前的秒数
/**
* Set the current date
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}//实现函数功能--设置当前的时间,参数是 date
/**
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}//实现函数功能--设置当前的时间,参数是各详细的变量
/**
* Get current year
*
* @return The current year
*/
//下面是得到 year、month、day 等值
public int getCurrentYear() {
// 获取当前日期对象mDate中的年份信息并返回该年份值
return mDate.get(Calendar.YEAR);
}
/**
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
// 如果不是初始化阶段mInitialising为false并且传入的年份与当前年份相同则直接返回不进行后续操作
if (!mInitialising && year == getCurrentYear()) {
return;
}
// 设置当前日期对象mDate中的年份为传入的年份值
mDate.set(Calendar.YEAR, year);
// 更新日期相关的显示控件,比如可能是界面上用于展示日期选择的相关组件等
updateDateControl();
// 触发日期时间改变的回调,通知外部监听者日期时间已更改(如果有设置相应监听器的话)
onDateTimeChanged();
}
/**
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
// 获取当前日期对象mDate中的月份信息注意返回值范围是0 - 110表示一月并返回
return mDate.get(Calendar.MONTH);
}
/**
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
// 如果不是初始化阶段mInitialising为false并且传入的月份与当前月份相同则直接返回不进行后续操作
if (!mInitialising && month == getCurrentMonth()) {
return;
}
// 设置当前日期对象mDate中的月份为传入的月份值
mDate.set(Calendar.MONTH, month);
// 更新日期相关的显示控件
updateDateControl();
// 触发日期时间改变的回调
onDateTimeChanged();
}
/**
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
// 获取当前日期对象mDate中表示当前是这个月第几天的信息并返回该天数
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
// 如果不是初始化阶段mInitialising为false并且传入的天数与当前天数相同则直接返回不进行后续操作
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
// 设置当前日期对象mDate中表示当前是这个月第几天的信息为传入的天数
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
// 更新日期相关的显示控件
updateDateControl();
// 触发日期时间改变的回调
onDateTimeChanged();
}
/**
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
// 获取当前日期对象mDate中24小时制下当前的小时数范围0 - 23并返回
return mDate.get(Calendar.HOUR_OF_DAY);
}
private int getCurrentHour() {
if (mIs24HourView) {
// 如果是24小时制视图直接返回24小时制下当前的小时数
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
// 如果小时数大于半天的小时数可能是12将其转换为12小时制下对应的小时数减去12
return hour - HOURS_IN_HALF_DAY;
} else {
// 如果小时数是0点转换为12小时制下对应的表示可能是12点否则直接返回当前小时数
return hour == 0? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
// 如果不是初始化阶段mInitialising为false并且传入的小时数与当前24小时制下的小时数相同则直接返回不进行后续操作
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
// 设置当前日期对象mDate中24小时制下的小时数为传入的小时数
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
// 如果小时数大于等于半天的小时数可能是12标记为下午mIsAm设为false
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
// 如果大于12点将其转换为12小时制下对应的小时数减去12
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
// 如果小时数小于半天的小时数可能是12标记为上午mIsAm设为true
mIsAm = true;
if (hourOfDay == 0) {
// 如果小时数是0点转换为12小时制下对应的表示可能是12点
hourOfDay = HOURS_IN_HALF_DAY;
}
}
// 更新AM/PM相关的显示控件
updateAmPmControl();
}
// 更新小时数的Spinner控件值可能是界面上用于选择小时的下拉框等组件
mHourSpinner.setValue(hourOfDay);
// 触发日期时间改变的回调
onDateTimeChanged();
}
/**
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
// 获取当前日期对象mDate中的分钟数信息并返回该分钟数值
return mDate.get(Calendar.MINUTE);
}
/**
* Set current minute
*/
public void setCurrentMinute(int minute) {
// 如果不是初始化阶段mInitialising为false并且传入的分钟数与当前分钟数相同则直接返回不进行后续操作
if (!mInitialising && minute == getCurrentMinute()) {
return;
}
// 设置当前日期对象mDate中的分钟数为传入的分钟数值
mDate.set(Calendar.MINUTE, minute);
// 更新分钟数的Spinner控件值类似前面小时数的相关操作可能是界面上用于选择分钟的组件
mMinuteSpinner.setValue(minute);
// 触发日期时间改变的回调
onDateTimeChanged();
}
/**
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
// 返回表示是否为24小时制视图的变量mIs24HourView的值true表示是24小时制false表示是12小时制AM/PM
return mIs24HourView;
}
/**
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
public void set24HourView(boolean is24HourView) {
// 如果传入的是否为24小时制的参数与当前状态mIs24HourView的值一致则直接返回不进行后续操作
if (mIs24HourView == is24HourView) {
return;
}
// 更新表示是否为24小时制视图的变量mIs24HourView的值
mIs24HourView = is24HourView;
// 根据是否为24小时制视图来设置AM/PM控件的可见性是24小时制则隐藏View.GONE否则显示View.VISIBLE
mAmPmSpinner.setVisibility(is24HourView? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
// 更新小时相关的控件显示(比如设置小时数选择范围等)
updateHourControl();
// 设置当前小时数,确保在切换视图模式后小时数显示等正确更新
setCurrentHour(hour);
// 更新AM/PM相关的显示控件
updateAmPmControl();
}
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
// 设置新创建的Calendar实例cal的时间为当前日期对象mDate对应的时间
cal.setTimeInMillis(mDate.getTimeInMillis());
// 将日期往前推DAYS_IN_ALL_WEEK / 2 + 1具体作用可能与日期显示范围等相关
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
// 先清空日期显示相关的Spinner控件的显示值
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
// 逐个添加日期,每次将日期往后推一天
cal.add(Calendar.DAY_OF_YEAR, 1);
// 格式化日期为指定格式MM.dd EEEE可能是月.日 星期几的格式),并存储到用于显示日期的数组中
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
// 设置日期显示相关的Spinner控件的显示值为前面格式化好的日期数组
mDateSpinner.setDisplayedValues(mDateDisplayValues);
// 设置日期显示相关的Spinner控件的默认选中值可能是中间位置等
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
// 使日期显示相关的Spinner控件失效可能触发重绘等更新操作确保显示最新的日期信息
mDateSpinner.invalidate();
}
private void updateAmPmControl() {
if (mIs24HourView) {
// 如果是24小时制视图隐藏AM/PM控件
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm? Calendar.AM : Calendar.PM;
// 根据当前是上午mIsAm为true还是下午mIsAm为false来设置AM/PM控件的值
mAmPmSpinner.setValue(index);
// 显示AM/PM控件
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
private void updateHourControl() {
if (mIs24HourView) {
// 如果是24小时制视图设置小时数Spinner控件的最小值和最大值为适合24小时制的范围
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
// 如果是12小时制视图设置小时数Spinner控件的最小值和最大值为适合12小时制的范围
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
* Set the callback that indicates the 'Set' button has been pressed.
* @param callback the callback, if null will do nothing
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
// 设置日期时间改变的回调监听器当日期时间发生改变时会调用该监听器的onDateTimeChanged方法
mOnDateTimeChangedListener = callback;
}
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener!= null) {
// 如果设置了日期时间改变的回调监听器就调用其onDateTimeChanged方法传递当前类实例以及当前的年、月、日、小时、分钟等信息
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}

@ -0,0 +1,119 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.util.Calendar;
import net.micode.notes.R;
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
// 继承自AlertDialog实现了OnClickListener接口该类用于创建一个日期时间选择对话框
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 用于存储当前选择的日期时间信息初始化为系统当前时间的Calendar实例
private Calendar mDate = Calendar.getInstance();
// 用于标记是否为24小时制视图
private boolean mIs24HourView;
// 用于设置当用户在对话框中点击确定按钮后,接收日期时间设置结果的监听器
private OnDateTimeSetListener mOnDateTimeSetListener;
// 日期时间选择器的实例,用于用户在对话框中实际操作选择日期时间
private DateTimePicker mDateTimePicker;
// 定义一个接口,用于外部实现,当用户在对话框中点击确定按钮后,将回调此接口的方法来传递设置好的日期时间信息
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
// 构造函数,用于创建日期时间选择对话框实例
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建一个DateTimePicker实例用于展示和操作日期时间选择界面
mDateTimePicker = new DateTimePicker(context);
// 将DateTimePicker设置为对话框的视图内容也就是对话框中展示供用户操作的部分
setView(mDateTimePicker);
// 为DateTimePicker设置日期时间改变的监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 当日期时间在DateTimePicker中发生改变时更新mDate中的相应信息
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());
}
});
// 设置初始的日期时间传入的date参数可能是某个特定的时间戳用于初始化对话框显示的初始时间
mDate.setTimeInMillis(date);
// 将秒数设置为0确保时间的秒部分是固定值通常是为了简化操作只关注到分钟级别精度等情况
mDate.set(Calendar.SECOND, 0);
// 将初始日期时间设置到DateTimePicker中使其初始显示对应的日期时间
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框的确定按钮文本并将当前类实现了OnClickListener接口作为点击事件监听器
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置对话框的取消按钮文本传入null表示使用默认的取消按钮行为一般就是关闭对话框
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统当前设置的时间格式是否为24小时制来设置对话框的时间显示格式
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 根据初始的日期时间信息,更新对话框标题显示的日期时间格式
updateTitle(mDate.getTimeInMillis());
}
// 设置对话框的时间显示格式是否为24小时制
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 设置当用户点击确定按钮后接收日期时间设置结果的监听器
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// 根据传入的日期时间戳,更新对话框标题显示的日期时间格式
private void updateTitle(long date) {
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
// 根据是否为24小时制视图添加对应的时间格式标记
flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
// 使用DateUtils工具类格式化日期时间并设置为对话框的标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// 实现OnClickListener接口的方法当用户点击对话框中的按钮此处主要针对确定按钮时会被调用
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener!= null) {
// 如果设置了OnDateTimeSetListener监听器当用户点击确定按钮时将当前对话框实例和设置好的日期时间戳传递给监听器回调方法
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,119 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import java.util.Calendar;
import net.micode.notes.R;
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
// 继承自AlertDialog实现了OnClickListener接口该类用于创建一个日期时间选择对话框
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 用于存储当前选择的日期时间信息初始化为系统当前时间的Calendar实例
private Calendar mDate = Calendar.getInstance();
// 用于标记是否为24小时制视图
private boolean mIs24HourView;
// 用于设置当用户在对话框中点击确定按钮后,接收日期时间设置结果的监听器
private OnDateTimeSetListener mOnDateTimeSetListener;
// 日期时间选择器的实例,用于用户在对话框中实际操作选择日期时间
private DateTimePicker mDateTimePicker;
// 定义一个接口,用于外部实现,当用户在对话框中点击确定按钮后,将回调此接口的方法来传递设置好的日期时间信息
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
// 构造函数,用于创建日期时间选择对话框实例
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建一个DateTimePicker实例用于展示和操作日期时间选择界面
mDateTimePicker = new DateTimePicker(context);
// 将DateTimePicker设置为对话框的视图内容也就是对话框中展示供用户操作的部分
setView(mDateTimePicker);
// 为DateTimePicker设置日期时间改变的监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 当日期时间在DateTimePicker中发生改变时更新mDate中的相应信息
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());
}
});
// 设置初始的日期时间传入的date参数可能是某个特定的时间戳用于初始化对话框显示的初始时间
mDate.setTimeInMillis(date);
// 将秒数设置为0确保时间的秒部分是固定值通常是为了简化操作只关注到分钟级别精度等情况
mDate.set(Calendar.SECOND, 0);
// 将初始日期时间设置到DateTimePicker中使其初始显示对应的日期时间
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 设置对话框的确定按钮文本并将当前类实现了OnClickListener接口作为点击事件监听器
setButton(context.getString(R.string.datetime_dialog_ok), this);
// 设置对话框的取消按钮文本传入null表示使用默认的取消按钮行为一般就是关闭对话框
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
// 根据系统当前设置的时间格式是否为24小时制来设置对话框的时间显示格式
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 根据初始的日期时间信息,更新对话框标题显示的日期时间格式
updateTitle(mDate.getTimeInMillis());
}
// 设置对话框的时间显示格式是否为24小时制
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 设置当用户点击确定按钮后接收日期时间设置结果的监听器
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// 根据传入的日期时间戳,更新对话框标题显示的日期时间格式
private void updateTitle(long date) {
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
// 根据是否为24小时制视图添加对应的时间格式标记
flag |= mIs24HourView? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
// 使用DateUtils工具类格式化日期时间并设置为对话框的标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// 实现OnClickListener接口的方法当用户点击对话框中的按钮此处主要针对确定按钮时会被调用
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener!= null) {
// 如果设置了OnDateTimeSetListener监听器当用户点击确定按钮时将当前对话框实例和设置好的日期时间戳传递给监听器回调方法
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,102 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// FoldersListAdapter类继承自CursorAdapter用于将数据库游标中的数据适配并展示到特定的视图中
// 这里推测是用于展示文件夹相关信息列表(从类名和代码逻辑推测)
public class FoldersListAdapter extends CursorAdapter {
// 定义一个字符串数组用于指定从数据库查询时要获取的列名这里只获取了笔记Note的ID列和摘要Snippet
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 定义常量表示在游标结果集中ID列对应的索引位置方便后续代码中通过索引获取对应列的数据
public static final int ID_COLUMN = 0;
// 定义常量,表示在游标结果集中名称列(从代码逻辑看应该是对应文件夹名称相关列)对应的索引位置
public static final int NAME_COLUMN = 1;
// 构造函数接收一个上下文Context对象和一个数据库游标Cursor对象
// 调用父类CursorAdapter的构造函数进行初始化
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO Auto-generated constructor stub这里可能后续需要补充一些初始化相关的逻辑代码目前为空
}
// 重写CursorAdapter的方法用于创建一个新的视图View对象
// 在这里返回了一个自定义的FolderListItem视图对象用于展示每一项的数据
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
// 重写CursorAdapter的方法用于将游标Cursor中的数据绑定到指定的视图View
// 也就是填充视图展示对应的数据内容
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) {
// 获取文件夹名称根据游标中ID列的值是否等于特定的根文件夹IDNotes.ID_ROOT_FOLDER来决定显示的名称内容
// 如果是根文件夹显示特定的字符串从资源中获取通常用于国际化等情况否则显示游标中对应名称列NAME_COLUMN的数据
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER)? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
// 调用FolderListItem视图对象的bind方法将获取到的文件夹名称传递进去进行文本显示等设置
((FolderListItem) view).bind(folderName);
}
}
// 根据指定的位置position从游标数据中获取对应位置的文件夹名称并返回
// 逻辑与bindView方法中获取文件夹名称的逻辑类似先获取游标对象再根据ID判断显示对应的名称
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);
}
// 定义一个内部私有类FolderListItem继承自LinearLayout用于表示列表中的每一项视图
// 可以理解为自定义的列表项布局类,用于展示文件夹相关信息
private class FolderListItem extends LinearLayout {
// 定义一个TextView对象用于显示文件夹名称
private TextView mName;
// 构造函数接收一个上下文Context对象初始化FolderListItem视图对象
public FolderListItem(Context context) {
super(context);
// 通过inflate方法将指定的布局资源R.layout.folder_list_item加载到当前的LinearLayout中
// 使其具备相应的布局结构和子视图
inflate(context, R.layout.folder_list_item, this);
// 通过findViewById方法找到布局中ID为tv_folder_name的TextView对象赋值给mName变量以便后续操作
mName = (TextView) findViewById(R.id.tv_folder_name);
}
// 定义一个方法用于将传入的名称name设置到mName对应的TextView上实现文本显示
public void bind(String name) {
mName.setText(name);
}
}
}

@ -0,0 +1,329 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Paint;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// NoteEditActivity类继承自Activity实现了多个接口用于实现笔记编辑相关的功能
// 比如处理笔记内容编辑、设置背景颜色、字体大小、提醒功能以及与桌面快捷方式等交互操作
public class NoteEditActivity extends Activity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
// 内部类,用于持有笔记头部相关视图的引用,方便后续操作和更新这些视图元素
private class HeadViewHolder {
public TextView tvModified;
public ImageView ivAlertIcon;
public TextView tvAlertDate;
public ImageView ibSetBgColor;
}
// 定义一个静态的Map用于将背景颜色选择按钮的ID映射到对应的颜色资源ID方便根据按钮操作获取相应颜色
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED);
sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE);
sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN);
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
// 定义一个静态的Map用于将颜色资源ID映射到对应的已选中背景颜色选择按钮的显示ID
// 用于在选择颜色后显示相应的选中状态图标等
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select);
sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select);
sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select);
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
// 定义一个静态的Map用于将字体大小选择按钮所在布局的ID映射到对应的字体大小资源ID方便操作字体大小选择逻辑
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
sFontSizeBtnsMap.put(R.id.ll_font_small, ResourceParser.TEXT_SMALL);
sFontSizeBtnsMap.put(R.id.ll_font_normal, ResourceParser.TEXT_MEDIUM);
sFontSizeBtnsMap.put(R.id.ll_font_super, ResourceParser.TEXT_SUPER);
}
// 定义一个静态的Map用于将字体大小资源ID映射到对应的已选中字体大小选择按钮的显示ID用于显示选中状态图标等
private static final Map<Integer, Integer> sFontSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sFontSelectorSelectionMap.put(ResourceParser.TEXT_LARGE, R.id.iv_large_select);
sFontSelectorSelectionMap.put(ResourceParser.TEXT_SMALL, R.id.iv_small_select);
sFontSelectorSelectionMap.put(ResourceParser.TEXT_MEDIUM, R.id.iv_medium_select);
sFontSelectorSelectionMap.put(ResourceParser.TEXT_SUPER, R.id.iv_super_select);
}
// 用于日志记录的标签方便在Log输出中识别该类相关的日志信息
private static final String TAG = "NoteEditActivity";
// 持有笔记头部视图相关元素的实例通过HeadViewHolder类进行封装
private HeadViewHolder mNoteHeaderHolder;
// 笔记头部视图的整体布局对象引用
private View mHeadViewPanel;
// 背景颜色选择器的视图对象引用,用于展示可选的背景颜色选项等
private View mNoteBgColorSelector;
// 字体大小选择器的视图对象引用,用于展示可选的字体大小选项等
private View mFontSizeSelector;
// 用于编辑笔记内容的EditText对象用户在此输入和编辑笔记文本
private EditText mNoteEditor;
// 笔记编辑区域的整体布局对象引用,可能包含编辑框以及相关的装饰等元素
private View mNoteEditorPanel;
// 代表正在编辑的笔记对象,封装了笔记相关的数据和操作逻辑
private WorkingNote mWorkingNote;
// 用于获取和操作应用的共享偏好设置,可存储一些用户个性化配置等信息,比如字体大小设置
private SharedPreferences mSharedPrefs;
// 记录当前选择的字体大小资源ID用于后续设置笔记文本的字体大小展示效果
private int mFontSizeId;
// 定义共享偏好设置中用于存储字体大小的键名
private static final String PREFERENCE_FONT_SIZE = "pref_font_size";
// 定义快捷方式图标标题的最大长度,用于生成桌面快捷方式时对笔记标题长度进行限制
private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10;
// 定义两个字符串常量,分别表示复选框选中和未选中的标记符号,用于处理列表模式下的笔记内容展示
public static final String TAG_CHECKED = String.valueOf('\u221A');
public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
// 用于展示笔记内容为列表模式时的线性布局,包含多个可编辑的文本项(可能每行一个文本项等情况)
private LinearLayout mEditTextList;
// 用于记录用户搜索时的查询字符串,可能用于在笔记内容中高亮显示查询结果等功能
private String mUserQuery;
// 用于编译用户查询字符串为正则表达式模式,方便在笔记内容中进行匹配查找操作
private Pattern mPattern;
// Activity创建时调用的方法进行一些初始化操作如设置布局、恢复Activity状态等
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置该Activity对应的布局文件这里是R.layout.note_edit布局资源
this.setContentView(R.layout.note_edit);
// 如果savedInstanceState为null即首次创建Activity且初始化Activity状态失败则结束该Activity并返回
if (savedInstanceState == null &&!initActivityState(getIntent())) {
finish();
return;
}
// 初始化相关资源,比如查找视图、设置监听器等操作
initResources();
}
/**
* Activity
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// 如果savedInstanceState不为null且包含特定的额外数据通过Intent.EXTRA_UID判断则尝试恢复Activity状态
if (savedInstanceState!= null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
if (!initActivityState(intent)) {
finish();
return;
}
Log.d(TAG, "Restoring from killed activity");
}
}
// 初始化Activity的状态根据传入的Intent不同情况来加载相应的笔记数据等操作
private boolean initActivityState(Intent intent) {
mWorkingNote = null;
// 如果Intent的动作是Intent.ACTION_VIEW表示查看已有笔记
if (TextUtils.equals(Intent.ACTION_VIEW, intent.getAction())) {
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
mUserQuery = "";
// 如果Intent包含搜索相关的额外数据SearchManager.EXTRA_DATA_KEY则说明是从搜索结果进入的
if (intent.hasExtra(SearchManager.EXTRA_DATA_KEY)) {
noteId = Long.parseLong(intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
mUserQuery = intent.getStringExtra(SearchManager.USER_QUERY);
}
// 检查指定ID的笔记是否在笔记数据库中可见如果不可见则跳转到笔记列表页面并提示笔记不存在然后结束当前Activity
if (!DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Intent jump = new Intent(this, NotesListActivity.class);
startActivity(jump);
showToast(R.string.error_note_not_exist);
finish();
return false;
} else {
// 加载指定ID的笔记数据到mWorkingNote对象如果加载失败则记录错误日志并结束当前Activity
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
}
// 设置软键盘的显示模式,这里是隐藏软键盘且根据内容调整布局大小
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
// 如果Intent的动作是Intent.ACTION_INSERT_OR_EDIT表示新建或编辑笔记
long folderId = intent.getLongExtra(Notes.INTENT_EXTRA_FOLDER_ID, 0);
int widgetId = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
int widgetType = intent.getIntExtra(Notes.INTENT_EXTRA_WIDGET_TYPE,
Notes.TYPE_WIDGET_INVALIDE);
int bgResId = intent.getIntExtra(Notes.INTENT_EXTRA_BACKGROUND_ID,
ResourceParser.getDefaultBgId(this));
// 解析通话记录笔记相关信息如果通话日期和电话号码都存在则尝试根据这些信息获取对应的笔记ID并加载笔记数据
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
long callDate = intent.getLongExtra(Notes.INTENT_EXTRA_CALL_DATE, 0);
if (callDate!= 0 && phoneNumber!= null) {
if (TextUtils.isEmpty(phoneNumber)) {
Log.w(TAG, "The call record number is null");
}
long noteId = 0;
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
finish();
return false;
}
} else {
// 如果根据通话信息找不到对应的笔记,则创建一个空的笔记对象,并设置相关属性,比如转换为通话笔记类型等
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
widgetType, bgResId);
mWorkingNote.convertToCallNote(phoneNumber, callDate);
}
} else {
// 如果通话信息不全,则创建一个空的笔记对象,并设置相关属性
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
bgResId);
}
// 设置软键盘的显示模式,这里是根据内容调整布局大小且显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
// 如果Intent的动作不被支持则记录错误日志并结束当前Activity
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
// 为正在编辑的笔记对象设置设置状态改变的监听器,以便在笔记相关设置改变时做出响应
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
// Activity重新恢复到前台时调用的方法用于重新初始化笔记编辑界面相关展示内容等操作
@Override
protected void onResume() {
super.onResume();
initNoteScreen();
}
// 初始化笔记编辑界面的展示内容,比如设置字体样式、显示笔记内容、更新日期时间显示等操作
private void initNoteScreen() {
// 根据当前选择的字体大小资源ID设置笔记编辑框的文本外观样式
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
// 如果笔记处于列表模式(复选框列表形式),则切换到列表模式展示笔记内容,否则正常展示笔记文本并处理查询结果高亮显示等
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
// 遍历背景颜色选择相关的已选中按钮显示ID集合将所有已选中按钮设置为不可见初始状态
for (Integer id : sBgSelectorSelectionMap.keySet()) {
findViewById(sBgSelectorSelectionMap.get(id)).setVisibility(View.GONE);
}
// 根据笔记对象中的相关属性设置笔记头部视图和编辑区域的背景资源
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
// 设置笔记头部显示的修改日期时间文本格式,展示日期、时间、年份等信息
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_YEAR));
// 显示提醒相关的头部信息(如提醒图标、提醒时间等),根据笔记是否设置了提醒来决定显示与否
showAlertHeader();
}
// 根据笔记是否设置了提醒以及提醒时间与当前时间的关系,来显示或隐藏提醒相关的头部视图元素(图标和时间文本)
private void showAlertHeader() {
if (mWorkingNote.hasClockAlert()) {
long time = System.currentTimeMillis();
if (time > mWorkingNote.getAlertDate()) {
mNoteHeaderHolder.tvAlertDate.setText(R.string.note_alert_expired);
} else {
mNoteHeaderHolder.tvAlertDate.setText(DateUtils.getRelativeTimeSpanString(
mWorkingNote.getAlertDate(), time

@ -0,0 +1,258 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;
import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
// NoteEditText类继承自EditText是一个自定义的文本编辑框控件在原有EditText功能基础上
// 增加了一些特定的交互逻辑,比如与外部监听者交互,处理按键事件、触摸事件以及上下文菜单等功能,
// 主要用于笔记编辑场景下的文本编辑操作
public class NoteEditText extends EditText {
// 用于日志记录的标签方便在Log输出中识别该类相关的日志信息
private static final String TAG = "NoteEditText";
// 用于记录当前文本编辑框在整个编辑列表中的索引位置,方便外部操作和定位该文本框
private int mIndex;
// 用于记录在按下删除键KeyEvent.KEYCODE_DEL之前文本选择的起始位置可能用于判断删除操作相关逻辑
private int mSelectionStartBeforeDelete;
// 定义一些常见的链接协议URL scheme字符串常量用于识别文本中的不同类型链接
private static final String SCHEME_TEL = "tel:";
private static final String SCHEME_HTTP = "http:";
private static final String SCHEME_EMAIL = "mailto:";
// 定义一个静态的Map用于将链接协议字符串映射到对应的资源ID该资源ID可能用于在界面上显示相应链接类型的描述等
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
// 定义一个接口用于与外部如包含该文本编辑框的Activity等进行交互通知外部文本编辑框相关的文本变化事件
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
void onEditTextDelete(int index, String text);
/**
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
void onEditTextEnter(int index, String text);
/**
* Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
// 用于存储外部设置的文本变化监听器实例,当文本编辑框内发生相应文本变化事件时,会通知该监听器
private OnTextViewChangeListener mOnTextViewChangeListener;
// 构造函数接收一个上下文Context对象初始化文本编辑框同时设置默认的索引值为0
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// 设置当前文本编辑框在整个编辑列表中的索引位置
public void setIndex(int index) {
mIndex = index;
}
// 设置文本变化监听器外部通过实现OnTextViewChangeListener接口并传入该方法来监听文本编辑框的相关事件
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
// 构造函数接收一个上下文Context对象和一个属性集AttributeSet对象
// 调用父类EditText相应构造函数进行初始化并应用默认的编辑文本样式android.R.attr.editTextStyle
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
// 构造函数接收一个上下文Context对象、一个属性集AttributeSet对象和一个默认样式defStyle参数
// 调用父类EditText相应构造函数进行初始化该构造函数目前没有额外的自定义初始化逻辑TODO部分可能后续添加
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
// 重写触摸事件处理方法,用于处理文本编辑框上的触摸操作,主要是实现点击文本区域时设置文本光标位置的功能
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸点的相对坐标(相对于文本编辑框内部),减去内边距并加上滚动偏移量,转换为文本布局中的坐标
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
// 根据触摸点的坐标获取所在的文本行以及该行上的字符偏移量,用于定位光标位置
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
// 设置文本光标的位置为计算得到的字符偏移量位置
Selection.setSelection(getText(), off);
break;
}
// 调用父类EditText的触摸事件处理方法确保原有触摸事件相关功能正常执行比如触摸滑动等操作
return super.onTouchEvent(event);
}
// 重写按下按键事件处理方法,用于捕获特定按键按下时的操作,目前主要是记录删除键按下时的文本选择起始位置
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 如果设置了文本变化监听器按下回车键时返回false可能是为了让外部决定是否拦截该按键事件的后续默认行为
if (mOnTextViewChangeListener!= null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录按下删除键时文本选择的起始位置,用于后续判断是否执行特定的删除逻辑
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
// 调用父类EditText的按下按键事件处理方法确保原有按键按下相关功能正常执行
return super.onKeyDown(keyCode, event);
}
// 重写松开按键事件处理方法,用于处理特定按键松开时的操作,比如回车键和删除键松开时的文本编辑相关逻辑
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener!= null) {
// 如果设置了文本变化监听器且当前文本选择起始位置为0且当前文本编辑框不是第一个索引不为0
// 则通知监听器执行删除当前文本编辑框的操作并返回true表示已处理该事件拦截默认的删除行为
if (0 == mSelectionStartBeforeDelete && mIndex!= 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener!= null) {
// 如果设置了文本变化监听器,获取当前文本选择起始位置,提取从该位置到文本末尾的内容作为新添加文本的内容,
// 然后将原文本截断到选择起始位置之前,通知监听器添加新的文本编辑框并传入相关信息
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart));
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
// 调用父类EditText的松开按键事件处理方法确保原有按键松开相关功能正常执行
return super.onKeyUp(keyCode, event);
}
// 重写焦点改变事件处理方法,用于在文本编辑框获得或失去焦点时,根据文本内容是否为空通知监听器文本状态变化
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener!= null) {
// 如果设置了文本变化监听器,根据文本编辑框是否获得焦点以及文本内容是否为空,通知监听器相应的文本状态变化情况
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
// 调用父类EditText的焦点改变事件处理方法确保原有焦点改变相关功能正常执行
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
// 重写创建上下文菜单事件处理方法,用于在长按文本编辑框时创建上下文菜单,主要是处理文本中包含链接时的菜单选项创建
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
// 获取当前文本选择的起始和结束位置,并取最小值和最大值,用于后续获取选择范围内的链接信息
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选择范围内的所有URLSpan对象代表文本中的链接
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
// 遍历链接协议与资源ID映射的Map查找与当前链接匹配的协议获取对应的资源ID作为菜单选项的显示文本资源ID
for (String schema : sSchemaActionResMap.keySet()) {
if (urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
// 如果没有找到匹配的协议对应的资源ID则使用默认的其他链接类型资源ID
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 在上下文菜单中添加一个菜单项设置其显示文本资源ID并设置点击菜单项的监听器点击时执行链接的跳转操作
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 执行链接的点击跳转操作调用URLSpan的onClick方法传入当前文本编辑框实例
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
// 调用父类EditText的创建上下文菜单事件处理方法确保原有上下文菜单相关功能正常执行比如添加默认的复制、粘贴等选项
super.onCreateContextMenu(menu);
}
}

@ -0,0 +1,297 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import net.micode.notes.data.Contact;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
// NoteItemData类用于封装从数据库游标Cursor中获取的笔记相关数据并提供一系列方法来获取这些数据以及判断笔记的一些状态信息
// 比如是否是最后一项、是否是通话记录等情况,方便在展示笔记列表等场景下使用
public class NoteItemData {
// 定义一个字符串数组,用于指定从数据库查询笔记相关信息时要获取的列名集合,涵盖了笔记的各种属性列
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
};
// 定义常量表示在游标结果集中笔记ID列对应的索引位置方便后续代码中通过索引获取对应列的数据
private static final int ID_COLUMN = 0;
// 定义常量,表示在游标结果集中提醒日期列对应的索引位置
private static final int ALERTED_DATE_COLUMN = 1;
// 定义常量表示在游标结果集中背景颜色ID列对应的索引位置
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;
// 定义常量表示在游标结果集中父文件夹ID列对应的索引位置
private static final int PARENT_ID_COLUMN = 7;
// 定义常量,表示在游标结果集中笔记摘要列对应的索引位置
private static final int SNIPPET_COLUMN = 8;
// 定义常量,表示在游标结果集中笔记类型列对应的索引位置
private static final int TYPE_COLUMN = 9;
// 定义常量表示在游标结果集中桌面小部件ID列对应的索引位置
private static final int WIDGET_ID_COLUMN = 10;
// 定义常量,表示在游标结果集中桌面小部件类型列对应的索引位置
private static final int WIDGET_TYPE_COLUMN = 11;
// 用于存储笔记的ID从数据库游标中获取对应列的数据进行赋值
private long mId;
// 用于存储笔记的提醒日期,从数据库游标中获取对应列的数据进行赋值
private long mAlertDate;
// 用于存储笔记的背景颜色ID从数据库游标中获取对应列的数据进行赋值
private int mBgColorId;
// 用于存储笔记的创建日期,从数据库游标中获取对应列的数据进行赋值
private long mCreatedDate;
// 用于标记笔记是否有附件根据游标中对应列的数据转换为布尔值进行赋值大于0表示有附件
private boolean mHasAttachment;
// 用于存储笔记的修改日期,从数据库游标中获取对应列的数据进行赋值
private long mModifiedDate;
// 用于存储与笔记相关的数量信息(具体含义可能根据业务逻辑确定),从数据库游标中获取对应列的数据进行赋值
private int mNotesCount;
// 用于存储笔记所属父文件夹的ID从数据库游标中获取对应列的数据进行赋值
private long mParentId;
// 用于存储笔记的摘要信息,从数据库游标中获取对应列的数据进行赋值,并对其中特定标记(可能是复选框相关标记)进行替换处理
private String mSnippet;
// 用于存储笔记的类型,从数据库游标中获取对应列的数据进行赋值
private int mType;
// 用于存储笔记关联的桌面小部件ID从数据库游标中获取对应列的数据进行赋值
private int mWidgetId;
// 用于存储笔记关联的桌面小部件类型,从数据库游标中获取对应列的数据进行赋值
private int mWidgetType;
// 用于存储与笔记相关的名称信息(比如通话记录对应的联系人名称等),初始化为空字符串,后续根据情况赋值
private String mName;
// 用于存储与笔记相关的电话号码信息(如果是通话记录笔记),初始化为空字符串,后续根据情况赋值
private String mPhoneNumber;
// 用于标记笔记是否是列表中的最后一项,通过调用游标相关方法判断并赋值
private boolean mIsLastItem;
// 用于标记笔记是否是列表中的第一项,通过调用游标相关方法判断并赋值
private boolean mIsFirstItem;
// 用于标记列表中是否只有这一项笔记,通过判断游标中的数据数量来赋值
private boolean mIsOnlyOneItem;
// 用于标记是否是某个文件夹后面仅跟着一个笔记的情况,通过特定逻辑判断赋值(后续介绍)
private boolean mIsOneNoteFollowingFolder;
// 用于标记是否是某个文件夹后面跟着多个笔记的情况,通过特定逻辑判断赋值(后续介绍)
private boolean mIsMultiNotesFollowingFolder;
// 构造函数接收一个上下文Context对象和一个数据库游标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)? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 对摘要信息中的特定标记可能是复选框相关标记从NoteEditActivity类中定义的常量获取进行替换处理去除这些标记
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 = "";
// 如果笔记所属父文件夹ID是通话记录文件夹的特定IDNotes.ID_CALL_RECORD_FOLDER则尝试获取对应的电话号码
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);
}
// 私有方法,用于检查笔记在游标所代表的数据列表中的位置相关状态信息,比如是否是第一项、最后一项、与文件夹的关联情况等
private void checkPostion(Cursor cursor) {
// 判断游标是否指向最后一项数据将结果赋值给mIsLastItem属性
mIsLastItem = cursor.isLast()? true : false;
// 判断游标是否指向第一项数据将结果赋值给mIsFirstItem属性
mIsFirstItem = cursor.isFirst()? true : false;
// 判断游标中的数据总数是否为1即是否只有一项数据将结果赋值给mIsOnlyOneItem属性
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 如果笔记类型是普通笔记Notes.TYPE_NOTE且不是第一项数据则进行以下判断逻辑
if (mType == Notes.TYPE_NOTE &&!mIsFirstItem) {
int position = cursor.getPosition();
// 将游标向前移动一位(移动到前一条数据),用于查看前一条数据的类型信息
if (cursor.moveToPrevious()) {
// 判断前一条数据的类型是否是文件夹类型或者系统类型(可能用于判断当前笔记是否紧跟在文件夹后面)
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 判断游标中的数据总数是否大于当前位置加1即当前笔记后面是否还有其他数据
// 如果是则表示当前文件夹后面跟着多个笔记,否则表示只跟着一个笔记
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;
}
// 获取笔记的ID返回对应的长整型数值
public long getId() {
return mId;
}
// 获取笔记的提醒日期,返回对应的长整型数值
public long getAlertDate() {
return mAlertDate;
}
// 获取笔记的创建日期,返回对应的长整型数值
public long getCreatedDate() {
return mCreatedDate;
}
// 判断笔记是否有附件,返回对应的布尔值
public boolean hasAttachment() {
return mHasAttachment;
}
// 获取笔记的修改日期,返回对应的长整型数值
public long getModifiedDate() {
return mModifiedDate;
}
// 获取笔记的背景颜色ID返回对应的整型数值
public int getBgColorId() {
return mBgColorId;
}
// 获取笔记所属父文件夹的ID返回对应的长整型数值
public long getParentId() {
return mParentId;
}
// 获取与笔记相关的数量信息(具体含义可能根据业务逻辑确定),返回对应的整型数值
public int getNotesCount() {
return mNotesCount;
}
// 获取笔记所属父文件夹的ID与getParentId方法功能相同可能为了方便调用取了不同方法名返回对应的长整型数值
public long getFolderId () {
return mParentId;
}
// 获取笔记的类型,返回对应的整型数值
public int getType() {
return mType;
}
// 获取笔记关联的桌面小部件类型,返回对应的整型数值
public int getWidgetType() {
return mWidgetType;
}
// 获取笔记关联的桌面小部件ID返回对应的整型数值
public int getWidgetId() {
return mWidgetId;
}
// 获取笔记的摘要信息,返回对应的字符串
public String getSnippet() {
return mSnippet;
}
// 判断笔记是否设置了提醒通过提醒日期是否大于0判断返回对应的布尔值
public boolean hasAlert() {
return (mAlertDate > 0);
}
// 判断笔记是否是通话记录通过父文件夹ID和电话号码是否为空判断返回对应的布尔值
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER &&!TextUtils.isEmpty(mPhoneNumber));
}
// 静态方法,用于从给定的游标中获取笔记类型信息(通过游标中对应列的数据获取),返回对应的整型数值
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}

@ -0,0 +1,346 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
// NotesListActivity类是一个安卓的Activity用于展示笔记列表实现了诸多与笔记列表交互相关的功能
// 比如点击查看笔记、长按进行多选操作、创建新笔记、删除笔记、移动笔记、文件夹相关操作(创建、重命名、删除等)以及与桌面小部件的交互等功能
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
// 用于异步查询文件夹中的笔记列表的查询令牌,作为标识特定的查询任务,方便在查询结果处理时区分不同来源的查询
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
// 用于异步查询文件夹列表的查询令牌
private static final int FOLDER_LIST_QUERY_TOKEN = 1;
// 定义菜单中删除文件夹选项对应的ID
private static final int MENU_FOLDER_DELETE = 0;
// 定义菜单中查看文件夹选项对应的ID
private static final int MENU_FOLDER_VIEW = 1;
// 定义菜单中重命名文件夹选项对应的ID
private static final int MENU_FOLDER_CHANGE_NAME = 2;
// 用于存储一个偏好设置的键,可能用于判断是否首次使用应用时展示介绍信息等相关逻辑
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
// 定义一个枚举类型,表示列表编辑的不同状态,包括普通笔记列表、子文件夹以及通话记录文件夹这几种情况
private enum ListEditState {
NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER
};
// 用于记录当前列表的编辑状态初始化为普通笔记列表状态NOTE_LIST
private ListEditState mState;
// 用于处理后台数据库查询操作的异步查询处理器实例,方便在后台线程中执行数据库查询任务并在查询完成后回调处理结果
private BackgroundQueryHandler mBackgroundQueryHandler;
// 用于适配笔记列表数据并展示在ListView中的适配器实例负责将数据和视图进行绑定管理列表项的显示等相关逻辑
private NotesListAdapter mNotesListAdapter;
// 用于展示笔记列表的ListView控件实例用户可以在该列表中查看和操作笔记
private ListView mNotesListView;
// 用于点击创建新笔记的按钮实例,用户点击该按钮可以触发创建新笔记的操作
private Button mAddNewNote;
// 用于标记是否进行事件分发的布尔变量,可能在触摸事件处理等相关逻辑中使用,判断是否将事件传递给其他视图处理
private boolean mDispatch;
// 用于记录触摸事件起始的Y坐标可能用于触摸滑动等相关逻辑判断
private int mOriginY;
// 用于记录在分发触摸事件时的Y坐标配合mOriginY等变量在触摸事件处理逻辑中使用
private int mDispatchY;
// 用于显示标题栏的TextView控件实例根据不同状态展示不同的标题内容如文件夹名称等
private TextView mTitleBar;
// 用于记录当前所在文件夹的ID初始化为根文件夹IDNotes.ID_ROOT_FOLDER随着用户操作如进入子文件夹等而改变
private long mCurrentFolderId;
// 用于获取内容提供器Content Provider实例通过它可以与安卓系统中的数据如数据库等进行交互操作
private ContentResolver mContentResolver;
// 用于实现ListView的多选模式相关回调逻辑的实例处理诸如创建多选模式、菜单项点击、选项状态改变等事件
private ModeCallback mModeCallBack;
// 用于日志记录的标签方便在Log输出中识别该类相关的日志信息
private static final String TAG = "NotesListActivity";
// 定义笔记列表视图滚动的速率,可能用于控制列表滚动的速度等相关逻辑(具体使用场景需结合代码其他部分确定)
public static final int NOTES_LISTVIEW_SCROLL_RATE = 30;
// 用于存储当前获取焦点的笔记数据项实例,在长按笔记等操作时记录对应的笔记数据,方便后续相关操作使用
private NoteItemData mFocusNoteDataItem;
// 定义一个用于查询普通文件夹下笔记的条件字符串通过父文件夹ID来筛选属于该文件夹下的笔记
private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
// 定义一个用于查询根文件夹下笔记的条件字符串逻辑相对复杂些除了排除系统类型笔记、根据父文件夹ID筛选外
// 还针对通话记录文件夹且有笔记数量大于0的情况进行了特殊处理
private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ NoteColumns.NOTES_COUNT + ">0)";
// 定义打开已有笔记的请求码用于在startActivityForResult方法中标识请求意图方便在onActivityResult方法中区分不同的返回结果
private final static int REQUEST_CODE_OPEN_NODE = 102;
// 定义创建新笔记的请求码
private final static int REQUEST_CODE_NEW_NODE = 103;
// Activity的创建方法在Activity首次创建时调用用于进行一些初始化操作如设置布局、初始化资源等
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.note_list);
initResources();
/**
* Insert an introduction when user firstly use this application
*/
setAppInfoFromRawRes();
}
// 处理其他Activity返回结果的方法根据请求码和返回结果码来决定是否进行特定的操作比如更新笔记列表适配器数据等
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK
&& (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
mNotesListAdapter.changeCursor(null);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
// 从原始资源文件中读取应用介绍信息,并创建一个空的笔记保存该介绍内容,同时将一个偏好设置标记为已展示介绍信息,
// 可能用于首次使用应用时给用户展示一些引导性的内容
private void setAppInfoFromRawRes() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
try {
// 打开原始资源文件R.raw.introduction准备读取其中的内容
in = getResources().openRawResource(R.raw.introduction);
if (in!= null) {
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
char[] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
sb.append(buf, 0, len);
}
} else {
Log.e(TAG, "Read introduction file error");
return;
}
} catch (IOException e) {
e.printStackTrace();
return;
} finally {
if (in!= null) {
try {
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
// 创建一个空的工作笔记实例传入相关参数如所在文件夹ID、桌面小部件相关信息等并设置其文本内容为读取到的介绍信息
WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_ROOT_FOLDER,
AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
ResourceParser.RED);
note.setWorkingText(sb.toString());
// 尝试保存该笔记如果保存成功则将偏好设置中表示已展示介绍信息的标记设置为true
if (note.saveNote()) {
sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
} else {
Log.e(TAG, "Save introduction note error");
return;
}
}
}
// Activity启动时调用的方法在onCreate之后用于启动异步查询笔记列表的操作开始获取并展示笔记数据
@Override
protected void onStart() {
super.onStart();
startAsyncNotesListQuery();
}
// 初始化各种资源相关的实例和控件属性,包括获取内容提供器、创建异步查询处理器、查找布局中的控件实例、
// 设置列表项点击和长按监听、设置适配器等操作,为后续的列表展示和交互操作做准备
private void initResources() {
mContentResolver = this.getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(this.getContentResolver());
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mNotesListView = (ListView) findViewById(R.id.notes_list);
// 给ListView添加底部视图可能用于展示一些额外信息如提示等传入布局资源并设置相关参数
mNotesListView.addFooterView(LayoutInflater.from(this).inflate(R.layout.note_list_footer, null),
null, false);
mNotesListView.setOnItemClickListener(new OnListItemClickListener());
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
mDispatch = false;
mDispatchY = 0;
mOriginY = 0;
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback();
}
// 内部类ModeCallback实现了ListView的多选模式相关接口ListView.MultiChoiceModeListener和OnMenuItemClickListener
// 用于处理在ListView处于多选模式下的各种操作逻辑如创建多选模式时的菜单初始化、菜单项点击、选项状态改变以及多选模式销毁等事件
private class ModeCallback implements ListView.MultiChoiceModeListener, OnMenuItemClickListener {
private DropdownMenu mDropDownMenu;
private ActionMode mActionMode;
private MenuItem mMoveMenu;
// 当多选模式创建时调用的方法,用于初始化多选模式相关的菜单、设置菜单项的可见性和点击监听、
// 设置自定义视图以及创建下拉菜单等操作返回true表示成功创建多选模式
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
mMoveMenu.setVisible(false);
} else {
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
}
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
mAddNewNote.setVisibility(View.GONE);
View customView = LayoutInflater.from(NotesListActivity.this).inflate(
R.layout.note_list_dropdown_menu, null);
mode.setCustomView(customView);
mDropDownMenu = new DropdownMenu(NotesListActivity.this,
(Button) customView.findViewById(R.id.selection_menu),
R.menu.note_list_dropdown);
mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
updateMenu();
return true;
}
});
return true;
}
// 根据已选中的笔记数量更新下拉菜单的标题和选择全部/取消选择全部菜单项的状态及标题,用于展示更准确的选择信息给用户
private void updateMenu() {
int selectedCount = mNotesListAdapter.getSelectedCount();
// Update dropdown menu
String format = getResources().getString(R.string.menu_select_title, selectedCount);
mDropDownMenu.setTitle(format);
MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
if (item!= null) {
if (mNotesListAdapter.isAllSelected()) {
item.setChecked(true);
item.setTitle(R.string.menu_deselect_all);
} else {
item.setChecked(false);
item.setTitle(R.string.menu_select_all);
}
}
}
// 在准备多选模式时调用的方法目前该方法未实现具体逻辑返回false可根据需要在后续添加相关准备操作代码
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
// 当多选模式下菜单项被点击时调用的方法目前该方法未实现具体逻辑返回false需根据不同菜单项功能添加相应处理代码
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO Auto-generated method stub
return false;
}
// 当多选模式销毁时调用的方法用于恢复ListView和相关按钮的状态比如设置适配器的选择模式为false、
// 恢复ListView的长按可点击属性、重新显示添加新笔记按钮等操作
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
mAddNewNote.setVisibility(View.VISIBLE);
}
// 完成多选模式的方法调用ActionMode的finish方法来结束多选模式
public void finishActionMode() {
mActionMode.finish();
}
// 当ListView中的选项状态改变如选中或取消选中时调用的方法用于更新适配器中对应项的选中状态并调用updateMenu方法更新菜单显示
public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
boolean checked) {
mNotesListAdapter.setCheckedItem(position, checked);
updateMenu();
}
// 处理菜单项点击事件的方法根据不同菜单项的ID

@ -0,0 +1,216 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import net.micode.notes.data.Notes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
// NotesListAdapter类继承自CursorAdapter用于将数据库游标Cursor中的数据适配到ListView等可展示列表的控件上
// 同时实现了与列表项选择相关的一系列功能比如记录选中项、全选操作、获取选中项的相关信息如ID集合、桌面小部件属性集合等以及判断选择状态等功能
public class NotesListAdapter extends CursorAdapter {
// 用于日志记录的标签方便在Log输出中识别该类相关的日志信息
private static final String TAG = "NotesListAdapter";
// 用于存储上下文Context对象方便后续操作中获取系统资源、创建视图等相关操作使用
private Context mContext;
// 用于以键值对形式(位置索引为键,是否选中为值)记录列表项的选中状态,方便管理和查询哪些项被选中了
private HashMap<Integer, Boolean> mSelectedIndex;
// 用于记录笔记的数量,通过计算游标中的笔记类型数据项个数来确定,方便后续判断全选等状态
private int mNotesCount;
// 用于标记当前列表是否处于选择(多选)模式,控制相关选择操作逻辑以及界面显示等情况
private boolean mChoiceMode;
// 内部静态类AppWidgetAttribute用于封装桌面小部件的相关属性如小部件ID和小部件类型方便在处理与桌面小部件相关的数据时使用
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
};
// 构造函数接收一个上下文Context对象调用父类CursorAdapter的构造函数进行初始化同时初始化选中状态记录的Map和其他相关属性
public NotesListAdapter(Context context) {
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
}
// 创建新的列表项视图的方法由系统在需要创建新的列表项视图时调用这里返回一个NotesListItem类型的新视图实例
// 具体的视图初始化等逻辑可能在NotesListItem类的构造函数或相关方法中实现
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
// 将游标中的数据绑定到给定的列表项视图上的方法根据视图类型进行类型转换后创建NoteItemData对象获取对应数据
// 再调用视图的bind方法将数据以及选择模式、是否选中状态等信息传递给视图进行展示相关设置
@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()));
}
}
// 设置指定位置的列表项的选中状态将位置索引和对应的选中状态存入mSelectedIndex这个Map中并通知数据集已改变
// 触发视图更新来反映选中状态的变化
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
// 判断当前是否处于选择多选模式返回对应的布尔值通过检查mChoiceMode变量来确定
public boolean isInChoiceMode() {
return mChoiceMode;
}
// 设置选择多选模式的开启或关闭清除已有的选中状态记录通过清空mSelectedIndex这个Map并更新mChoiceMode变量的值
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
// 全选或全取消选择操作的方法,遍历游标中的所有数据项,根据数据项类型(是否是笔记类型)来设置对应位置的选中状态,
// 实现全部选中或全部取消选中笔记项的功能
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
}
// 获取所有被选中的列表项的ID集合以Long类型表示遍历mSelectedIndex这个记录选中状态的Map
// 对于选中的项获取其对应的ID并添加到HashSet集合中排除根文件夹ID这种不符合预期的情况最后返回该集合
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
// 获取所有被选中的列表项对应的桌面小部件属性集合遍历mSelectedIndex这个记录选中状态的Map
// 对于选中的项获取对应的游标数据通过游标数据创建NoteItemData对象进而获取桌面小部件相关属性封装到AppWidgetAttribute对象中并添加到HashSet集合
// 最后返回该集合(注意这里不会关闭游标,因为游标应由适配器统一管理关闭)
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c!= null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
// 获取被选中的列表项的数量通过获取mSelectedIndex这个Map中值即选中状态布尔值的集合遍历该集合统计值为true的数量最后返回统计结果
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
// 判断是否所有笔记项都被选中了通过比较已选中的笔记数量和总的笔记数量mNotesCount来确定返回对应的布尔值
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount!= 0 && checkedCount == mNotesCount);
}
// 判断指定位置的列表项是否被选中先检查该位置对应的选中状态值是否为null不存在则视为未选中否则返回对应位置记录的选中状态值
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
// 当内容发生改变时调用的方法由父类CursorAdapter在相关情况下触发先调用父类的对应方法然后重新计算笔记数量
// 以保证后续关于笔记数量相关的判断(如全选判断等)能基于最新的数据
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
// 当游标Cursor发生改变时调用的方法由外部调用或父类相关逻辑触发先调用父类的对应方法然后重新计算笔记数量
// 同样是为了更新笔记数量相关的状态信息,使其与新游标数据匹配
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
// 私有方法用于计算笔记的数量遍历游标中的所有数据项通过NoteItemData类获取对应数据项的类型
// 统计笔记类型Notes.TYPE_NOTE的数据项个数更新mNotesCount变量的值
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c!= null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}

@ -0,0 +1,152 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
// NotesListItem类继承自LinearLayout代表笔记列表中的每一个列表项视图负责展示笔记相关的各项信息
// 比如标题、时间、提醒图标、是否选中的复选框等,并根据笔记的不同类型(文件夹、普通笔记、通话记录文件夹等)以及不同状态(是否是第一个、最后一个等)来展示不同的样式和内容。
public class NotesListItem extends LinearLayout {
// 用于显示提醒图标的ImageView控件根据笔记是否有提醒来设置其可见性和显示的图片资源
private ImageView mAlert;
// 用于显示笔记标题的TextView控件展示笔记的主要内容摘要或者文件夹名称等信息
private TextView mTitle;
// 用于显示笔记修改时间的TextView控件通过格式化时间字符串来展示相对时间如“几分钟前”等形式
private TextView mTime;
// 用于显示通话记录相关联系人名称的TextView控件在对应的通话记录笔记场景下展示联系人名字
private TextView mCallName;
// 用于存储当前列表项对应的笔记数据对象NoteItemData类型方便在视图展示以及其他相关逻辑中获取笔记的各种属性数据
private NoteItemData mItemData;
// 用于显示是否选中当前列表项的CheckBox控件在多选模式下根据笔记类型和选中状态来设置其可见性和选中状态
private CheckBox mCheckBox;
// 构造函数接收一个上下文Context对象调用父类LinearLayout的构造函数进行初始化
// 同时通过inflater方法加载对应的布局资源R.layout.note_item到该视图中并查找布局中的各个子控件实例
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
// 用于将笔记数据绑定到该列表项视图上的方法,根据不同的条件(如选择模式、笔记类型、是否有提醒等)来设置各个子控件的显示内容、可见性以及背景样式等属性
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 如果处于选择模式choiceMode为true且当前数据对应的笔记类型是普通笔记Notes.TYPE_NOTE
// 则设置复选框可见并根据传入的选中状态checked参数设置复选框的选中状态否则隐藏复选框
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
} else {
mCheckBox.setVisibility(View.GONE);
}
// 将传入的笔记数据对象赋值给成员变量mItemData方便后续在其他方法中获取该笔记的相关属性
mItemData = data;
// 如果笔记的ID是通话记录文件夹的特定IDNotes.ID_CALL_RECORD_FOLDER则进行以下通话记录文件夹相关的视图设置
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);
}
// 如果笔记所属父文件夹的ID是通话记录文件夹的特定IDNotes.ID_CALL_RECORD_FOLDER则进行以下通话记录笔记相关的视图设置
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);
// 如果笔记类型是文件夹Notes.TYPE_FOLDER则设置标题文本为文件夹名称加上包含的笔记数量信息并隐藏提醒图标
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);
}
}
}
// 设置时间文本通过DateUtils工具类的方法将笔记的修改日期转换为相对时间字符串进行展示如“几分钟前”等形式
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 调用方法根据笔记数据来设置该列表项的背景样式,根据笔记类型以及在列表中的位置等状态来选择对应的背景资源
setBackground(data);
}
// 私有方法,用于根据笔记数据来设置该列表项的背景样式,根据笔记类型以及在列表中的位置等状态来选择对应的背景资源进行设置
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
// 如果笔记类型是普通笔记Notes.TYPE_NOTE则根据笔记在列表中的位置等状态来选择对应的背景资源进行设置
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());
}
}
// 用于获取当前列表项对应的笔记数据对象的方法返回存储的NoteItemData对象方便外部获取笔记的相关属性数据
public NoteItemData getItemData() {
return mItemData;
}
}

@ -0,0 +1,399 @@
/*
* Copyright (c) 2010 - 2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
// NotesPreferenceActivity类继承自PreferenceActivity用于展示和管理应用的偏好设置相关界面及功能
// 涉及与账户同步相关的设置操作,比如选择同步账户、查看上次同步时间、开始或取消同步等功能,同时可以响应同步服务的广播来更新界面显示状态。
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";
// 用于存储偏好设置中账户分类的PreferenceCategory实例方便后续对该分类下的偏好设置项进行添加、删除等操作
private PreferenceCategory mAccountCategory;
// 用于接收同步服务广播的广播接收器实例,通过注册该接收器来监听同步服务相关的广播消息并做出相应处理(如更新界面等)
private GTaskReceiver mReceiver;
// 用于存储原始的账户列表信息,可能在比较账户变化等逻辑中使用(比如判断是否新增了账户等情况)
private Account[] mOriAccounts;
// 用于标记是否添加了新账户的布尔变量,在相关逻辑中(如账户变化后自动设置同步账户等)根据该变量值来决定是否执行特定操作
private boolean mHasAddedAccount;
// 在Activity首次创建时调用的方法用于进行一些初始化操作如设置ActionBar的返回按钮可用、加载偏好设置资源文件、
// 查找账户分类实例、注册广播接收器以及添加列表头部视图等操作
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
// 当Activity从暂停状态恢复时调用的方法如从其他Activity返回时用于在特定情况下如有新账户添加自动设置同步账户
// 并调用方法刷新界面显示(更新账户偏好设置项、同步按钮状态等相关显示内容)
@Override
protected void onResume() {
super.onResume();
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts!= null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
}
}
refreshUI();
}
// 当Activity即将被销毁时调用的方法用于注销之前注册的广播接收器避免内存泄漏等问题然后调用父类的销毁方法完成销毁操作
@Override
protected void onDestroy() {
if (mReceiver!= null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
// 用于加载账户偏好设置项的方法,先清空账户分类下原有的偏好设置项,然后创建一个新的偏好设置项(用于选择同步账户),
// 设置其标题、摘要以及点击监听事件,点击时根据不同情况(是否正在同步、是否已设置账户等)弹出相应的对话框或提示信息,
// 最后将该偏好设置项添加到账户分类下
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
mAccountCategory.addPreference(accountPref);
}
// 用于加载同步按钮相关设置和显示状态的方法查找布局中的同步按钮和上次同步时间显示的TextView控件根据同步服务是否正在同步来设置按钮的文本、
// 点击监听事件(开始同步或取消同步)以及按钮的可用性(是否已设置同步账户),同时根据同步状态设置上次同步时间的显示文本和可见性。
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime!= 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
// 用于刷新整个偏好设置界面显示的方法,调用上述的加载账户偏好设置项和加载同步按钮相关设置的方法,更新界面上的相关显示内容以反映最新的状态。
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
// 显示用于选择同步账户的对话框方法创建一个AlertDialog.Builder实例来构建对话框设置自定义标题视图包含标题和副标题文本
// 获取可用的谷歌账户列表,根据当前已设置的同步账户标记选中项,设置单选列表项点击监听事件(选择账户后设置同步账户并刷新界面),
// 同时添加一个用于添加新账户的视图,点击该视图可跳转到添加账户的系统设置界面,最后显示构建好的对话框。
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
// 显示用于确认更改同步账户的对话框方法创建一个AlertDialog.Builder实例来构建对话框设置自定义标题视图包含标题和警告提示文本
// 设置对话框的菜单项(更改账户、删除账户、取消)及点击监听事件(根据点击的菜单项执行相应操作,如显示选择账户对话框、删除同步账户并刷新界面等),
// 最后显示构建好的对话框。
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
// 获取设备上已登录的谷歌账户列表的方法通过AccountManager获取账户列表筛选出类型为“com.google”的账户并返回该数组。
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
// 设置同步账户的方法,根据传入的账户名称与当前已设置的同步账户名称进行比较,如果不同则更新共享偏好设置中的同步账户名称信息,
// 同时清除上次同步时间、清理本地与同步任务gtask相关的信息通过更新数据库中相关字段最后显示设置成功的提示信息。
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account!= null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
// 删除同步账户的方法通过编辑共享偏好设置移除同步账户名称和上次同步时间相关的键值对信息然后清理本地与同步任务gtask相关的信息通过更新数据库中相关字段
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
// 静态方法,用于获取当前设置的同步账户名称,通过从共享偏好设置中根据对应的键获取字符串值(如果不存在则返回空字符串)来实现。
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
return false;
}
}
}

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

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

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

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#88555555" />
<item android:state_selected="true" android:color="#ff999999" />
<item android:color="#ff000000" />
</selector>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#50000000" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

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

Loading…
Cancel
Save