Compare commits

...

3 Commits
main ... lixiao

Author SHA1 Message Date
LiXiAo 588a611905 src
5 months ago
LiXiAo 6daa2350df 思维导图
5 months ago
LiXiAo 40e3669d96 test1.0
5 months ago

15
src/.gitignore vendored

@ -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 @@
XiaoMI-Note

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

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

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-24T05:28:02.754207300Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\86187\.android\avd\Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="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>
</GradleProjectSettings>
</option>
</component>
</project>

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

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

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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1 @@
/build

@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "net.micode.xiaomi_note"
compileSdk = 35
defaultConfig {
applicationId = "net.micode.xiaomi_note"
minSdk = 24
targetSdk = 35
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:\\Code\\AndroidCode\\Notesmaster\\httpcomponents-client-4.5.14-bin\\lib",
// "include" to listOf("*.aar", "*.jar"),
// "exclude" to listOf("")
// )))
//修改为如下代码:
implementation(files("E:/android/Notes/httpcomponents-client-4.5.14-bin/httpcomponents-client-4.5.14-bin/lib/httpclient-osgi-4.5.14.jar"))
implementation(files("E:/android/Notes/httpcomponents-client-4.5.14-bin/httpcomponents-client-4.5.14-bin/lib/httpclient-win-4.5.14.jar"))
implementation(files("E:/android/Notes/httpcomponents-client-4.5.14-bin/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.xiaomi_note;
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.xiaomi_note", appContext.getPackageName());
}
}

@ -0,0 +1,154 @@
<?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" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="XiaoMI-Note"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
tools:targetApi="31">
<activity
android:name="net.micode.xiaomi_note.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" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
</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.xiaomi_note.data.NotesProvider"
android:exported="false"
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>
<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.xiaomi_note.MainActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<service
android:name="net.micode.xiaomi_note.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,26 @@
package net.micode.xiaomi_note;
import android.os.Bundle;
import androidx.activity.EdgeToEdge; // 提供边缘到边缘显示的支持类
import androidx.appcompat.app.AppCompatActivity; // AndroidX提供的Activity基类
import androidx.core.graphics.Insets; // 用于处理窗口内边距的工具类
import androidx.core.view.ViewCompat; // 提供视图相关的兼容性API
import androidx.core.view.WindowInsetsCompat; // 窗口装饰区域的兼容性API
public class MainActivity extends AppCompatActivity { // 主活动类继承自AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) { // 活动创建时调用的方法
super.onCreate(savedInstanceState); // 调用父类方法完成基本初始化
EdgeToEdge.enable(this); // 启用边缘到边缘显示模式
setContentView(R.layout.activity_main); // 设置当前活动的布局文件
ViewCompat.setOnApplyWindowInsetsListener( // 为视图设置窗口装饰区域监听器
findViewById(R.id.main), // 获取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,73 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.xiaomi_note.data;
import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import java.util.HashMap;
public class Contact {
private static HashMap<String, String> sContactCache;
private static final String TAG = "Contact";
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
public static String getContact(Context context, String phoneNumber) {
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
if (cursor != null && cursor.moveToFirst()) {
try {
String name = cursor.getString(0);
sContactCache.put(phoneNumber, name);
return name;
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
cursor.close();
}
} else {
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}
}
}

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

@ -0,0 +1,362 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.xiaomi_note.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.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.DataConstants;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
public interface TABLE {
public static final String NOTE = "note";
public static final String DATA = "data";
}
private static final String TAG = "NotesDatabaseHelper";
private static NotesDatabaseHelper mInstance;
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* Increase folder's note count when move note to the folder
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when move note from folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
* Increase folder's note count when insert new note to the folder
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when delete note from the folder
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Delete datas belong to note which has been deleted
*/
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Delete notes belong to folder which has been deleted
*/
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Move notes belong to folder which has been moved to trash folder
*/
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS folder_move_notes_on_trash");
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* call record foler for call notes
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* root folder which is default folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* temporary folder which is used for moving note
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create trash folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
private void reCreateDataTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
createNoteTable(db);
createDataTable(db);
}
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// add a column for gtask id
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}

@ -0,0 +1,305 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.xiaomi_note.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.xiaomi_note.R;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.data.NotesDatabaseHelper.TABLE;
public class NotesProvider extends ContentProvider {
private static final UriMatcher mMatcher;
private NotesDatabaseHelper mHelper;
private static final String TAG = "NotesProvider";
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
private static final int URI_DATA_ITEM = 4;
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
/**
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
*/
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
String id = null;
switch (mMatcher.match(uri)) {
case URI_NOTE:
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
searchString = uri.getQueryParameter("pattern");
}
if (TextUtils.isEmpty(searchString)) {
return null;
}
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
return c;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
switch (mMatcher.match(uri)) {
case URI_NOTE:
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
if (values.containsKey(DataColumns.NOTE_ID)) {
noteId = values.getAsLong(DataColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong data format without note id:" + values.toString());
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// Notify the note uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null);
}
// Notify the data uri
if (dataId > 0) {
getContext().getContentResolver().notifyChange(
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
return ContentUris.withAppendedId(uri, insertedId);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
/**
* ID that smaller than 0 is system folder which is not allowed to
* trash
*/
long noteId = Long.valueOf(id);
if (noteId <= 0) {
break;
}
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:
count = db.delete(TABLE.DATA, selection, selectionArgs);
deleteData = true;
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.DATA,
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
switch (mMatcher.match(uri)) {
case URI_NOTE:
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
break;
case URI_DATA:
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(TABLE.NOTE);
sql.append(" SET ");
sql.append(NoteColumns.VERSION);
sql.append("=" + NoteColumns.VERSION + "+1 ");
if (id > 0 || !TextUtils.isEmpty(selection)) {
sql.append(" WHERE ");
}
if (id > 0) {
sql.append(NoteColumns.ID + "=" + String.valueOf(id));
}
if (!TextUtils.isEmpty(selection)) {
String selectString = id > 0 ? parseSelection(selection) : selection;
for (String args : selectionArgs) {
selectString = selectString.replaceFirst("\\?", args);
}
sql.append(selectString);
}
mHelper.getWritableDatabase().execSQL(sql.toString());
}
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}
}

@ -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.xiaomi_note.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.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.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.DataConstants;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.data.NotesDatabaseHelper.TABLE;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.gtask.exception.ActionFailureException;
import net.micode.xiaomi_note.tool.GTaskStringUtils;
import net.micode.xiaomi_note.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.xiaomi_note.gtask.data;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.DataConstants;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.gtask.exception.ActionFailureException;
import net.micode.xiaomi_note.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.xiaomi_note.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.gtask.exception.ActionFailureException;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.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,138 @@
/*
* 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.xiaomi_note.gtask.remote;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
// 新增以下导入
import android.os.Build;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import net.micode.xiaomi_note.R;
import net.micode.xiaomi_note.ui.NotesListActivity;
import net.micode.xiaomi_note.ui.NotesPreferenceActivity;
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
private static final 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) {
// 添加权限检查(此处为关键修改点)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
// 无权限时直接返回,避免崩溃
return;
}
}
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.xiaomi_note.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.xiaomi_note.gtask.data.Node;
import net.micode.xiaomi_note.gtask.data.Task;
import net.micode.xiaomi_note.gtask.data.TaskList;
import net.micode.xiaomi_note.gtask.exception.ActionFailureException;
import net.micode.xiaomi_note.gtask.exception.NetworkFailureException;
import net.micode.xiaomi_note.tool.GTaskStringUtils;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.R;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.gtask.data.MetaData;
import net.micode.xiaomi_note.gtask.data.Node;
import net.micode.xiaomi_note.gtask.data.SqlNote;
import net.micode.xiaomi_note.gtask.data.Task;
import net.micode.xiaomi_note.gtask.data.TaskList;
import net.micode.xiaomi_note.gtask.exception.ActionFailureException;
import net.micode.xiaomi_note.gtask.exception.NetworkFailureException;
import net.micode.xiaomi_note.tool.DataUtils;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.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.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.CallNote;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.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.xiaomi_note.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.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.CallNote;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.DataConstants;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.data.Notes.TextNote;
import net.micode.xiaomi_note.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,359 @@
/*
* 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.xiaomi_note.tool;
// 导入必要的Android上下文、数据库游标、文件系统相关类等
import android.content.Context;
import android.database.Cursor;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
// 导入应用资源、便签数据相关的类和常量
import net.micode.xiaomi_note.R;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.DataColumns;
import net.micode.xiaomi_note.data.Notes.DataConstants;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
// 导入Java文件系统操作类
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
// BackupUtils.java
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; // SD卡未挂载状态
// 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(); // 调用内部TextExport类的导出方法
}
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; // 笔记ID列索引
private static final int NOTE_COLUMN_MODIFIED_DATE = 1; // 修改日期列索引
private static final int NOTE_COLUMN_SNIPPET = 2; // 摘录列索引
private static final int NOTE_COLUMN_TYPE = 3; // 类型列索引
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; // MIME类型列索引
private static final int DATA_COLUMN_CALL_DATE = 2; // 呼叫日期列索引
private static final int DATA_COLUMN_PHONE_NUMBER = 4; // 电话号码列索引
private final String [] TEXT_FORMAT; // 文本格式数组
private static final int FORMAT_FOLDER_NAME = 0; // 文件夹名称格式索引
private static final int FORMAT_NOTE_DATE = 1; // 笔记日期格式索引
private static final int FORMAT_NOTE_CONTENT = 2; // 笔记内容格式索引
private 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); // 获取笔记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); // 获取MIME类型
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) { // 捕获IO异常
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; // 返回SD卡未挂载状态码
}
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); // 获取文件夹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); // 获取笔记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, // 生成挂载在SD卡上的文件
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对象用于拼接文件路径字符串
StringBuilder sb = new StringBuilder();
// 将外部存储目录路径追加到StringBuilder中
sb.append(Environment.getExternalStorageDirectory());
// 追加通过资源ID获取的文件夹路径字符串
sb.append(context.getString(filePathResId));
// 将StringBuilder转换为File对象表示目标文件夹
File filedir = new File(sb.toString());
// 追加带有日期格式的文件名到StringBuilder中
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
// 将StringBuilder转换为File对象表示目标文件
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) {
// 捕获IO异常并打印堆栈信息
e.printStackTrace();
}
// 如果发生异常或失败返回null
return null;
}
}

@ -0,0 +1,406 @@
/*
* 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.
*/
// DataUtils.java 文件,位于 net.micode.xiaomi_note.tool 包下
package net.micode.xiaomi_note.tool;
// 导入 Android 平台相关的类用于操作内容提供器、URI、值集等
import android.content.ContentProviderOperation; // 用于构建批量操作指令
import android.content.ContentProviderResult; // 批量操作结果
import android.content.ContentResolver; // 访问内容提供器的核心接口
import android.content.ContentUris; // 提供 URI 操作工具方法
import android.content.ContentValues; // 用于存储键值对数据
import android.content.OperationApplicationException; // 批量操作异常类
import android.database.Cursor; // 用于查询数据库结果集
import android.os.RemoteException; // 远程调用异常类
import android.util.Log; // 日志记录工具
// 导入项目内部自定义的数据模型和 UI 相关类
import net.micode.xiaomi_note.data.Notes; // Notes 数据模型
import net.micode.xiaomi_note.data.Notes.CallNote; // CallNote 子类
import net.micode.xiaomi_note.data.Notes.NoteColumns; // NoteColumns 静态常量类
import net.micode.xiaomi_note.ui.NotesListAdapter.AppWidgetAttribute; // AppWidgetAttribute 类
// 导入 Java 标准库集合类,用于处理集合操作
import java.util.ArrayList; // 动态数组
import java.util.HashSet; // 哈希集合
public class DataUtils {
// 定义日志标签,用于调试信息输出
public static final String TAG = "DataUtils";
/**
*
* @param resolver ContentResolver
* @param ids IDHashSet
* @return truefalse
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
// 如果传入的ids为空则记录日志并返回true可能表示无需操作
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
// 如果ids集合中没有元素则记录日志并返回true
if (ids.size() == 0) {
Log.d(TAG, "no id is in the hashset");
return true;
}
// 创建一个列表用于存储批量操作的命令
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
// 遍历ids集合中的每个id
for (long id : ids) {
// 如果id是系统根文件夹ID则跳过该条目
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue;
}
// 构建删除指定ID笔记的ContentProviderOperation
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 将构建好的操作添加到操作列表中
operationList.add(builder.build());
}
try {
// 执行批量操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 如果结果为空或操作失败则记录日志并返回false
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
// 操作成功返回true
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()));
}
// 异常情况下返回false
return false;
}
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
// 创建一个ContentValues对象用于存储更新字段及其值
ContentValues values = new ContentValues();
// 设置目标文件夹ID到NoteColumns.PARENT_ID字段
values.put(NoteColumns.PARENT_ID, desFolderId);
// 设置源文件夹ID到NoteColumns.ORIGIN_PARENT_ID字段
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
// 设置本地修改标志位为1表示该笔记已被修改
values.put(NoteColumns.LOCAL_MODIFIED, 1);
// 使用ContentResolver更新指定ID的笔记数据更新字段由values定义
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
// 判断传入的ids集合是否为空
if (ids == null) {
// 如果ids为空记录日志并返回true可能表示操作成功
Log.d(TAG, "the ids is null");
return true;
}
// 初始化一个操作列表,用于批量更新数据库
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
// 遍历ids集合中的每个id
for (long id : ids) {
// 创建ContentProviderOperation的Builder对象用于构建批量更新操作
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 设置目标文件夹ID到NoteColumns.PARENT_ID字段
builder.withValue(NoteColumns.PARENT_ID, folderId);
// 设置本地修改标志位为1表示该笔记已被修改
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
// 将构建完成的操作添加到操作列表中
operationList.add(builder.build());
}
try {
// 执行批量更新操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 检查结果是否为空或无效若无效则记录日志并返回false
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
// 如果操作成功返回true
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()));
}
// 如果发生异常返回false
return false;
}
/**
*
* @param resolver ContentResolver
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询所有符合条件的文件夹记录数
Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI, // 内容URI指定查询的数据来源
new String[] { "COUNT(*)" }, // 投影,这里只查询记录总数
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<?", // 查询条件类型等于普通文件夹且父ID不等于回收站ID
new String[] { // 查询参数,替换占位符
String.valueOf(Notes.TYPE_FOLDER), // 文件夹类型
String.valueOf(Notes.ID_TRASH_FOLER) // 回收站ID
},
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) {
// 查询指定 noteId 的笔记记录,并筛选条件为类型匹配且父文件夹不是回收站
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) { // 如果查询结果数量大于 0则表示存在符合条件的记录
exist = true;
}
cursor.close(); // 关闭游标释放资源
}
return exist; // 返回是否存在符合条件的记录
}
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 使用 ContentResolver 查询指定 ID 的笔记数据
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
// 初始化是否存在标志位
boolean exist = false;
if (cursor != null) { // 检查游标是否为空
if (cursor.getCount() > 0) { // 判断查询结果是否有数据
exist = true; // 如果有数据,则设置存在标志为 true
}
cursor.close(); // 关闭游标释放资源
}
return exist; // 返回是否存在结果
}
// 判断指定ID的数据是否存在于ContentProvider的数据库中
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
// 使用ContentResolver查询指定ID的数据URI
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false; // 初始化标识变量,用于记录数据是否存在
if (cursor != null) { // 检查Cursor对象是否为空
if (cursor.getCount() > 0) { // 如果查询结果数量大于0则表示数据存在
exist = true;
}
cursor.close(); // 关闭Cursor对象以释放资源
}
return exist; // 返回数据是否存在
}
// 定义一个静态方法用于检查指定名称的文件夹是否在ContentResolver中存在且可见
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
// 使用ContentResolver查询符合特定条件的笔记数据
Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI, // 指定查询的URI表示笔记内容的数据库表
null, // 查询所有列null表示不指定具体列
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER, // 条件:类型必须是文件夹类型
new String[] { name }, // 查询参数,指定文件夹名称
NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER // 条件父ID不能等于回收站文件夹ID
);
boolean exist = false; // 初始化标志位,用于判断文件夹是否存在
if(cursor != null) { // 如果Cursor对象不为空
if(cursor.getCount() > 0) { // 如果查询结果数量大于0说明存在符合条件的记录
exist = true; // 设置标志位为true
}
cursor.close(); // 关闭游标,释放资源
}
return exist; // 返回文件夹是否存在
}
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
// 查询 ContentProvider 中与指定文件夹 ID 对应的便签小部件数据
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, // 投影:需要查询的列
NoteColumns.PARENT_ID + "=?", // 条件parentId 等于 folderId
new String[] { String.valueOf(folderId) }, // 参数folderId 的值
null); // 不指定排序
// 初始化结果集合
HashSet<AppWidgetAttribute> set = null;
if (c != null) {
// 如果游标有数据,则移动到第一条记录
if (c.moveToFirst()) {
set = new HashSet<AppWidgetAttribute>(); // 创建一个空的集合用于存储结果
do {
try {
// 创建 AppWidgetAttribute 对象以存储查询结果
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0); // 获取小部件 ID
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) {
// 查询 ContentProvider 中的数据,指定 URI、需要查询的列、查询条件、条件参数以及排序方式
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER }, // 查询目标列:电话号码
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", // 查询条件noteId 和 MIME 类型匹配
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) {
// 查询 ContentProvider 中的数据筛选条件为通话日期、MIME 类型和电话号码
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID }, // 查询目标列:笔记 ID
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)", // 查询条件通话日期匹配、MIME 类型匹配、电话号码匹配
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, // 条件参数
null); // 不指定排序
if (cursor != null) {
// 如果查询结果存在且有数据行
if (cursor.moveToFirst()) {
try {
// 尝试获取第一行数据中的笔记 ID长整型
return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
// 如果获取失败,记录错误日志
Log.e(TAG, "Get call note id fails " + e.toString());
}
}
// 关闭游标释放资源
cursor.close();
}
// 如果未找到符合条件的笔记 ID则返回默认值 0
return 0;
}
public static String getSnippetById(ContentResolver resolver, long noteId) {
// 使用 ContentResolver 查询指定 ID 的笔记片段数据
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET }, // 查询目标列:仅获取笔记片段字段
NoteColumns.ID + "=?", // 条件语句ID 等于指定值
new String [] { String.valueOf(noteId)},// 替换占位符的参数:将 noteId 转为字符串
null); // 不设置排序参数
if (cursor != null) {
String snippet = ""; // 初始化笔记片段变量
// 如果查询结果存在数据,则移动游标到第一条记录
if (cursor.moveToFirst()) {
snippet = cursor.getString(0); // 获取第一条记录的笔记片段字段值
}
cursor.close(); // 关闭游标释放资源
return snippet; // 返回查询到的笔记片段
}
// 如果未找到对应笔记 ID抛出异常
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
*
* @param snippet
* @return nullnull
*
*/
public static String getFormattedSnippet(String snippet) {
// 检查输入字符串是否为null
if (snippet != null) {
// 去除字符串首尾的空白字符
snippet = snippet.trim();
// 查找字符串中第一个换行符的位置
int index = snippet.indexOf('\n');
// 如果找到换行符
if (index != -1) {
// 截取字符串从起始位置到换行符之前的部分
snippet = snippet.substring(0, index);
}
}
// 返回处理后的字符串
return snippet;
}
}

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

@ -0,0 +1,227 @@
/*
* 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.
*/
// 定义一个名为ResourceParser的公共类位于net.micode.xiaomi_note.tool包中
package net.micode.xiaomi_note.tool;
// 导入必要的Android上下文和偏好设置管理类
import android.content.Context;
import android.preference.PreferenceManager;
// 导入项目中定义的资源文件和偏好设置活动类
import net.micode.xiaomi_note.R;
import net.micode.xiaomi_note.ui.NotesPreferenceActivity;
// 定义一个名为ResourceParser的公共类
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 // 红色标题背景
};
// 根据传入的id获取对应的编辑页面背景资源
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
// 根据传入的id获取对应的编辑页面标题背景资源
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
// 定义一个静态方法用于获取默认背景资源ID
public static int getDefaultBgId(Context context) {
// 检查SharedPreferences中是否启用了背景颜色设置
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
// 如果启用则通过随机数生成一个背景资源ID
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
// 如果未启用,则返回默认背景颜色的常量值
return BG_DEFAULT_COLOR;
}
}
public static class NoteItemBgResources {
// 定义一个静态数组存储第一部分背景资源的ID
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 // 红色上部背景
};
// 定义一个静态数组存储中间部分背景资源的ID
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 // 红色中部背景
};
// 定义一个静态数组存储最后一部分背景资源的ID
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 // 红色下部背景
};
// 定义一个静态数组存储单一背景资源的ID
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single, // 黄色单一背景
R.drawable.list_blue_single, // 蓝色单一背景
R.drawable.list_white_single, // 白色单一背景
R.drawable.list_green_single, // 绿色单一背景
R.drawable.list_red_single // 红色单一背景
};
// 根据id获取第一部分背景资源的ID
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
// 根据id获取最后一部分背景资源的ID
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
// 根据id获取单一背景资源的ID
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
// 根据id获取中间部分背景资源的ID
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
// 获取文件夹背景资源的ID
public static int getFolderBgRes() {
return R.drawable.list_folder; // 文件夹背景
}
}
public static class WidgetBgResources {
// 定义一个静态数组存储2倍分辨率的背景资源ID
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow, // 2倍分辨率黄色背景资源
R.drawable.widget_2x_blue, // 2倍分辨率蓝色背景资源
R.drawable.widget_2x_white, // 2倍分辨率白色背景资源
R.drawable.widget_2x_green, // 2倍分辨率绿色背景资源
R.drawable.widget_2x_red // 2倍分辨率红色背景资源
};
/**
* id2ID
* @param id BG_2X_RESOURCES
* @return ID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
// 定义一个静态数组存储4倍分辨率的背景资源ID
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow, // 4倍分辨率黄色背景资源
R.drawable.widget_4x_blue, // 4倍分辨率蓝色背景资源
R.drawable.widget_4x_white, // 4倍分辨率白色背景资源
R.drawable.widget_4x_green, // 4倍分辨率绿色背景资源
R.drawable.widget_4x_red // 4倍分辨率红色背景资源
};
/**
* id4ID
* @param id BG_4X_RESOURCES
* @return ID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
public static class TextAppearanceResources {
// 定义一个静态数组存储不同的文本外观资源ID
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal, // 正常文本外观样式
R.style.TextAppearanceMedium, // 中等文本外观样式
R.style.TextAppearanceLarge, // 大文本外观样式
R.style.TextAppearanceSuper // 超大文本外观样式
};
/**
* IDID
* @param id ID
* @return IDID
*
* HACKME: SharedPreferencesIDbug
* ID{@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
public static int getTexAppearanceResource(int id) {
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE; // 返回默认背景字体大小
}
return TEXTAPPEARANCE_RESOURCES[id]; // 返回对应索引的资源ID
}
/**
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length; // 返回资源数组的长度
}
}
}

@ -0,0 +1,224 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.xiaomi_note.ui; // 定义包名
// 导入必要的类和接口
import android.app.Activity; // Android Activity类用于管理Activity生命周期
import android.app.AlertDialog; // Android AlertDialog类用于显示对话框
import android.content.Context; // Android Context类提供应用环境信息
import android.content.DialogInterface; // Android DialogInterface类处理对话框事件
import android.content.Intent; // Android Intent类用于在组件间传递消息
import android.media.AudioManager; // Android AudioManager类管理音频相关设置
import android.media.MediaPlayer; // Android MediaPlayer类用于播放媒体文件
import android.media.RingtoneManager; // Android RingtoneManager类管理设备铃声
import android.net.Uri; // Android Uri类表示统一资源标识符
import android.os.Bundle; // Android Bundle类用于保存和传递数据
import android.os.PowerManager; // Android PowerManager类管理电源状态
import android.provider.Settings; // Android Settings类访问系统设置
import android.view.Window; // Android Window类管理窗口属性
import android.view.WindowManager; // Android WindowManager类管理窗口布局
import net.micode.xiaomi_note.R; // 引入当前项目的资源文件
import net.micode.xiaomi_note.data.Notes; // 引入项目中的Notes类
import net.micode.xiaomi_note.tool.DataUtils; // 引入项目中的DataUtils工具类
import java.io.IOException; // Java标准库中的IOException类处理输入输出异常
// 定义一个继承自Activity的类AlarmAlertActivity并实现OnClickListener和OnDismissListener接口
public class AlarmAlertActivity extends Activity implements
DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
// 声明一个长整型变量mNoteId用于存储闹钟笔记ID
private long mNoteId;
// 声明一个字符串变量mSnippet用于存储闹钟的简短描述信息
private String mSnippet;
// 定义一个静态常量SNIPPET_PREW_MAX_LEN表示简短描述信息的最大长度60字符
private static final int SNIPPET_PREW_MAX_LEN = 60;
// 声明一个MediaPlayer对象mPlayer用于播放闹钟声音
MediaPlayer mPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 调用父类方法,初始化活动
super.onCreate(savedInstanceState);
// 隐藏标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取窗口对象并设置标志位,使应用在锁屏时也能显示
final Window win = getWindow();
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 判断屏幕是否处于点亮状态,若未点亮则设置相关标志位
if (!isScreenOn()) {
win.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON // 保持屏幕常亮
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON // 打开屏幕
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许在屏幕常亮时锁定设备
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 设置布局适应装饰区域
}
// 获取传递给当前活动的意图
Intent intent = getIntent();
try {
// 从意图中提取笔记ID并将其转换为长整型
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 根据笔记ID查询笔记摘要信息
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 如果摘要长度超过预设最大值,则截取并追加提示字符串
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ?
mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) +
getResources().getString(R.string.notelist_string_info) :
mSnippet;
} catch (IllegalArgumentException e) {
// 捕获非法参数异常并打印堆栈信息,直接退出方法
e.printStackTrace();
return;
}
// 初始化MediaPlayer对象
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);
// 调用电源管理器的isScreenOn方法判断屏幕是否开启
return pm.isScreenOn();
}
private void playAlarmSound() {
// 获取系统默认的闹钟声音URI
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 获取系统设置中影响铃声流的掩码值默认值为0
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 {
// 设置播放器的数据源为指定的闹钟声音URI
mPlayer.setDataSource(this, url);
// 准备播放器以播放音频
mPlayer.prepare();
// 设置播放器循环播放
mPlayer.setLooping(true);
// 开始播放音频
mPlayer.start();
} catch (IllegalArgumentException e) {
// 捕获非法参数异常并打印堆栈信息
e.printStackTrace();
} catch (SecurityException e) {
// 捕获安全异常并打印堆栈信息
e.printStackTrace();
} catch (IllegalStateException e) {
// 捕获非法状态异常并打印堆栈信息
e.printStackTrace();
} catch (IOException e) {
// 捕获输入输出异常并打印堆栈信息
e.printStackTrace();
}
}
private void showActionDialog() {
// 创建一个AlertDialog的构建器实例
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
// 设置对话框的标题R.string.app_name是一个字符串资源的引用
dialog.setTitle(R.string.app_name);
// 设置对话框的消息内容mSnippet是当前类的一个成员变量
dialog.setMessage(mSnippet);
// 设置对话框的“确定”按钮R.string.notealert_ok是按钮文本的资源引用并设置点击事件监听器为当前类
dialog.setPositiveButton(R.string.notealert_ok, this);
// 判断设备屏幕是否处于开启状态
if (isScreenOn()) {
// 如果屏幕开启则设置“进入”按钮R.string.notealert_enter是按钮文本的资源引用并设置点击事件监听器为当前类
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 显示对话框,并为对话框设置 dismiss 事件的监听器
dialog.show().setOnDismissListener(this);
}
public void onClick(DialogInterface dialog, int which) {
// 根据用户点击的按钮进行不同的处理
switch (which) {
// 如果点击的是负向按钮(通常是“取消”)
case DialogInterface.BUTTON_NEGATIVE:
// 创建一个新的Intent对象用于启动NoteEditActivity
Intent intent = new Intent(this, NoteEditActivity.class);
// 设置Intent的动作为ACTION_VIEW表示查看数据
intent.setAction(Intent.ACTION_VIEW);
// 添加额外的数据到Intent中这里传递了一个UID用于标识笔记
intent.putExtra(Intent.EXTRA_UID, mNoteId);
// 启动目标Activity
startActivity(intent);
break;
default:
// 其他情况不做任何处理
break;
}
}
// 当对话框被dismiss时触发的方法
public void onDismiss(DialogInterface dialog) {
// 停止闹钟声音的播放
stopAlarmSound();
// 结束当前Activity
finish();
}
// 停止闹钟声音的私有方法
private void stopAlarmSound() {
// 如果mPlayer对象不为空
if (mPlayer != null) {
// 停止播放声音
mPlayer.stop();
// 释放MediaPlayer资源
mPlayer.release();
// 将mPlayer置为null避免后续空指针异常
mPlayer = null;
}
}
}

@ -0,0 +1,84 @@
/*
* 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.xiaomi_note.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.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
// 定义一个广播接收器类用于初始化闹钟
public class AlarmInitReceiver extends BroadcastReceiver {
// 定义查询时需要的列名数组
private static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE // 闹钟触发日期
};
// 定义查询结果中列的位置常量
private static final int COLUMN_ID = 0; // 笔记ID所在列的位置
private static final int COLUMN_ALERTED_DATE = 1; // 闹钟触发日期所在列的位置
@Override
public void onReceive(Context context, Intent intent) {
// 获取当前系统时间
long currentDate = System.currentTimeMillis();
// 查询数据库中未触发的闹钟记录
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) }, // 设置查询条件:闹钟触发日期大于当前时间且类型为笔记
null); // 不指定排序方式
// 如果查询结果不为空
if (c != null) {
// 如果有数据可读取
if (c.moveToFirst()) {
do {
// 获取当前记录的闹钟触发日期
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建发送给闹钟接收器的意图
Intent sender = new Intent(context, AlarmReceiver.class);
// 为意图设置数据包含笔记ID
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 获取PendingIntent对象用于触发闹钟
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// 获取系统闹钟管理服务
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 设置闹钟在指定时间触发
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 遍历所有符合条件的记录
}
// 关闭游标释放资源
c.close();
}
}
}

@ -0,0 +1,39 @@
/*
* 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.xiaomi_note.ui;
// 导入BroadcastReceiver类用于接收广播事件
import android.content.BroadcastReceiver;
// 导入Context类提供应用程序的全局信息
import android.content.Context;
// 导入Intent类用于在不同组件之间传递消息
import android.content.Intent;
// 定义一个继承自BroadcastReceiver的类用于处理闹钟相关的广播事件
public class AlarmReceiver extends BroadcastReceiver {
// 重写onReceive方法当接收到广播时会被调用
@Override
public void onReceive(Context context, Intent intent) {
// 设置目标Activity为AlarmAlertActivity
intent.setClass(context, AlarmAlertActivity.class);
// 添加启动标志,确保新任务被创建
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动指定的Activity
context.startActivity(intent);
}
}

@ -0,0 +1,590 @@
/*
* 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.xiaomi_note.ui; // 定义包名,表示该文件属于此包
// 导入必要的类库,包括日期格式符号、日历等工具类
import java.text.DateFormatSymbols;
import java.util.Calendar;
// 引入资源文件,用于获取应用中的字符串和其他资源
import net.micode.xiaomi_note.R;
// 导入Android相关类用于UI组件和上下文操作
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
public class DateTimePicker extends FrameLayout {
// 默认启用状态
private static final boolean DEFAULT_ENABLE_STATE = true;
// 半天中的小时数
private static final int HOURS_IN_HALF_DAY = 12;
// 一天中的小时数
private static final int HOURS_IN_ALL_DAY = 24;
// 一周中的天数
private static final int DAYS_IN_ALL_WEEK = 7;
// 日期选择器的最小值
private static final int DATE_SPINNER_MIN_VAL = 0;
// 日期选择器的最大值(基于一周的天数)
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
// 24小时制下小时选择器的最小值
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
// 24小时制下小时选择器的最大值
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
// 12小时制下小时选择器的最小值
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
// 12小时制下小时选择器的最大值
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;
// AM/PM选择器的最小值
private static final int AMPM_SPINNER_MIN_VAL = 0;
// AM/PM选择器的最大值
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 日期选择器
private final NumberPicker mDateSpinner;
// 小时选择器
private final NumberPicker mHourSpinner;
// 分钟选择器
private final NumberPicker mMinuteSpinner;
// AM/PM选择器
private final NumberPicker mAmPmSpinner;
// 当前日期时间
private Calendar mDate;
// 日期显示值数组
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
// 是否为上午
private boolean mIsAm;
// 是否为24小时制
private boolean mIs24HourView;
// 是否启用
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
// 初始化标志
private boolean mInitialising;
// 日期时间改变监听器
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 调整日期选择器的日期值
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
// 更新日期控件显示
updateDateControl();
// 触发日期时间变化事件
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false; // 标记是否需要更新日期
Calendar cal = Calendar.getInstance(); // 获取当前时间的Calendar实例
if (!mIs24HourView) { // 如果是AM/PM模式
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
// 从下午切换到上午
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1); // 增加一天
isDateChanged = true;
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
// 从上午切换到下午
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1); // 减少一天
isDateChanged = true;
}
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
// AM/PM切换时更新AM/PM状态
mIsAm = !mIsAm;
updateAmPmControl();
}
} else { // 如果是24小时制
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
// 从23点切换到0点
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1); // 增加一天
isDateChanged = true;
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
// 从0点切换到23点
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1); // 减少一天
isDateChanged = true;
}
}
// 计算新的小时值
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
// 触发日期时间变化事件
onDateTimeChanged();
if (isDateChanged) {
// 如果日期发生变化,更新年、月、日显示
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
setCurrentDay(cal.get(Calendar.DAY_OF_MONTH));
}
}
};
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 获取分钟选择器的最小值
int minValue = mMinuteSpinner.getMinValue();
// 获取分钟选择器的最大值
int maxValue = mMinuteSpinner.getMaxValue();
// 初始化偏移量为0
int offset = 0;
// 如果旧值是最大值且新值是最小值,则表示跨过了一天的边界,增加一天
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
}
// 如果旧值是最小值且新值是最大值,则表示跨过了前一天的边界,减少一天
else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
// 如果有跨天偏移,则调整日期时间控件
if (offset != 0) {
// 调整日期时间对象的小时数
mDate.add(Calendar.HOUR_OF_DAY, offset);
// 更新小时选择器的值
mHourSpinner.setValue(getCurrentHour());
// 更新日期控件显示
updateDateControl();
// 获取当前小时
int newHour = getCurrentHourOfDay();
// 判断是否进入下午时段
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
// 更新AM/PM控件
updateAmPmControl();
}
// 否则仍在上午时段
else {
mIsAm = true;
// 更新AM/PM控件
updateAmPmControl();
}
}
// 设置新的分钟值到日期时间对象
mDate.set(Calendar.MINUTE, newVal);
// 触发日期时间改变事件
onDateTimeChanged();
}
};
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 切换 AM/PM 状态
mIsAm = !mIsAm;
// 根据当前是 AM 还是 PM 调整日期时间
if (mIsAm) {
// 如果切换到 AM则减少 12 小时(半天)
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
// 如果切换到 PM则增加 12 小时(半天)
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
// 更新 AM/PM 控件的状态显示
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) {
// 构造函数使用指定时间初始化并启用24小时制判断
this(context, date, DateFormat.is24HourFormat(context));
}
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
// 初始化成员变量
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 加载布局文件
inflate(context, R.layout.datetime_picker, this);
// 初始化日期选择器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化上午/下午选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
mAmPmSpinner.setMaxValue(AMPM_SPINNER_MAX_VAL);
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 更新控件到初始状态
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置24小时制模式
set24HourView(is24HourView);
// 设置当前日期
setCurrentDate(date);
// 设置是否可用
setEnabled(isEnabled());
// 设置内容描述
mInitialising = false;
}
@Override
public void setEnabled(boolean enabled) {
// 如果当前状态与设置的状态相同,则直接返回
if (mIsEnabled == enabled) {
return;
}
// 调用父类方法设置控件启用状态
super.setEnabled(enabled);
// 分别设置子控件的启用状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
// 更新内部状态变量
mIsEnabled = enabled;
}
@Override
public boolean isEnabled() {
// 返回当前控件的启用状态
return mIsEnabled;
}
/**
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
// 获取当前日期的时间戳(毫秒)
return mDate.getTimeInMillis();
}
/**
* Set the current date
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
// 创建一个新的Calendar实例并设置其时间为给定的时间戳
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
// 调用多参数setCurrentDate方法设置具体日期时间
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 设置年、月、日、小时、分钟
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
setCurrentHour(hourOfDay);
setCurrentMinute(minute);
}
/**
* Get current year
*
* @return The current year
*/
public int getCurrentYear() {
// 获取当前年份
return mDate.get(Calendar.YEAR);
}
/**
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
// 如果正在初始化且年份未变化,则直接返回
if (!mInitialising && year == getCurrentYear()) {
return;
}
// 设置新的年份
mDate.set(Calendar.YEAR, year);
// 更新日期控件显示
updateDateControl();
// 触发日期改变事件
onDateTimeChanged();
}
/**
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
// 获取当前月份
return mDate.get(Calendar.MONTH);
}
/**
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
// 如果正在初始化且月份未变化,则直接返回
if (!mInitialising && month == getCurrentMonth()) {
return;
}
// 设置新的月份
mDate.set(Calendar.MONTH, month);
// 更新日期控件显示
updateDateControl();
// 触发日期改变事件
onDateTimeChanged();
}
/**
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
// 获取当前日期在当月中的天数
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
// 如果正在初始化且天数未变化,则直接返回
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return;
}
// 设置新的日期
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
// 更新日期控件显示
updateDateControl();
// 触发日期改变事件
onDateTimeChanged();
}
/**
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY); // 获取当前时间的24小时制小时数
}
private int getCurrentHour() {
if (mIs24HourView){ // 如果是24小时制模式
return getCurrentHourOfDay(); // 直接返回24小时制的小时数
} else { // 如果是AM/PM模式
int hour = getCurrentHourOfDay(); // 获取24小时制的小时数
if (hour > HOURS_IN_HALF_DAY) { // 如果超过中午12点
return hour - HOURS_IN_HALF_DAY; // 转换为12小时制的下午小时数
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour; // 如果是凌晨0点显示为12点否则直接返回
}
}
}
/**
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) { // 如果不是初始化状态且输入值未改变
return; // 不做任何操作
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay); // 设置当前时间为指定的24小时制小时数
if (!mIs24HourView) { // 如果是AM/PM模式
if (hourOfDay >= HOURS_IN_HALF_DAY) { // 如果是下午或晚上
mIsAm = false; // 设置为PM模式
if (hourOfDay > HOURS_IN_HALF_DAY) { // 如果超过下午6点
hourOfDay -= HOURS_IN_HALF_DAY; // 调整为12小时制的下午小时数
}
} else { // 如果是上午
mIsAm = true; // 设置为AM模式
if (hourOfDay == 0) { // 如果是凌晨0点
hourOfDay = HOURS_IN_HALF_DAY; // 显示为12点
}
}
updateAmPmControl(); // 更新AM/PM控件的状态
}
mHourSpinner.setValue(hourOfDay); // 更新小时选择器的值
onDateTimeChanged(); // 触发日期时间变化回调
}
/**
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE); // 获取当前分钟数
}
/**
* Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) { // 如果不是初始化状态且输入值未改变
return; // 不做任何操作
}
mMinuteSpinner.setValue(minute); // 更新分钟选择器的值
mDate.set(Calendar.MINUTE, minute); // 设置当前时间为指定的分钟数
onDateTimeChanged(); // 触发日期时间变化回调
}
/**
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
return mIs24HourView; // 返回是否处于24小时制模式
}
/**
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) { // 如果模式未发生变化
return; // 不做任何操作
}
mIs24HourView = is24HourView; // 更新模式标志
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE); // 隐藏或显示AM/PM选择器
int hour = getCurrentHourOfDay(); // 获取当前24小时制的小时数
updateHourControl(); // 更新小时选择器的范围
setCurrentHour(hour); // 设置当前小时数
updateAmPmControl(); // 更新AM/PM控件的状态
}
private void updateDateControl() {
Calendar cal = Calendar.getInstance(); // 创建一个Calendar实例
cal.setTimeInMillis(mDate.getTimeInMillis()); // 初始化为当前日期时间
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1); // 计算基准日期
mDateSpinner.setDisplayedValues(null); // 清空日期选择器的显示值
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) { // 循环填充一周的日期
cal.add(Calendar.DAY_OF_YEAR, 1); // 每次增加一天
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal); // 格式化日期为"月.日 星期X"
}
mDateSpinner.setDisplayedValues(mDateDisplayValues); // 设置日期选择器的显示值
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 设置默认选中值为中心日期
mDateSpinner.invalidate(); // 请求重新绘制
}
private void updateAmPmControl() {
if (mIs24HourView) { // 如果是24小时制模式
mAmPmSpinner.setVisibility(View.GONE); // 隐藏AM/PM选择器
} else { // 如果是AM/PM模式
int index = mIsAm ? Calendar.AM : Calendar.PM; // 判断当前是AM还是PM
mAmPmSpinner.setValue(index); // 设置AM/PM选择器的值
mAmPmSpinner.setVisibility(View.VISIBLE); // 显示AM/PM选择器
}
}
private void updateHourControl() {
if (mIs24HourView) { // 如果是24小时制模式
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW); // 设置最小值为24小时制的最小值
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW); // 设置最大值为24小时制的最大值
} else { // 如果是AM/PM模式
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW); // 设置最小值为12小时制的最小值
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW); // 设置最大值为12小时制的最大值
}
}
/**
* Set the callback that indicates the 'Set' button has been pressed.
* @param callback the callback, if null will do nothing
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback; // 设置日期时间变化的回调监听器
}
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) { // 如果存在回调监听器
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(), // 触发回调,传递当前年、月、日、小时、分钟等信息
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
}

@ -0,0 +1,124 @@
/*
* 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.xiaomi_note.ui; // 定义包名
// 导入所需的Java类库
import java.util.Calendar; // 导入日历类,用于日期和时间操作
// 导入项目内部资源和自定义类
import net.micode.xiaomi_note.R; // 导入资源文件
import net.micode.xiaomi_note.ui.DateTimePicker; // 导入日期时间选择器类
import net.micode.xiaomi_note.ui.DateTimePicker.OnDateTimeChangedListener; // 导入日期时间改变监听接口
// 导入Android系统类库
import android.app.AlertDialog; // 导入对话框类
import android.content.Context; // 导入上下文类
import android.content.DialogInterface; // 导入对话框事件监听接口
import android.content.DialogInterface.OnClickListener; // 导入对话框按钮点击事件监听器接口
import android.text.format.DateFormat; // 导入日期格式化工具类
import android.text.format.DateUtils; // 导入日期工具类
// 定义一个继承自AlertDialog的对话框类用于日期时间选择
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 当前选择的日期时间
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);
// 初始化日期时间选择器
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker); // 将日期时间选择器设置为对话框的内容视图
// 设置日期时间变化监听器
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部存储的日期时间
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
updateTitle(mDate.getTimeInMillis()); // 更新对话框标题
}
});
// 初始化日期时间为传入的时间戳并设置秒数为0
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis()); // 设置初始日期时间到选择器
// 设置确定按钮和取消按钮
setButton(context.getString(R.string.datetime_dialog_ok), this); // 确定按钮绑定点击事件
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮无操作
// 根据系统设置判断是否使用24小时制
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 初始化对话框标题
updateTitle(mDate.getTimeInMillis());
}
// 设置是否使用24小时制
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
// 设置日期时间设置监听器
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
// 更新对话框标题的方法
private void updateTitle(long date) {
// 定义日期时间显示格式标志位
int flag =
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR; // 根据24小时制设置格式
// 使用DateUtils格式化日期时间并设置为对话框标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
// 处理对话框按钮点击事件
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
// 调用监听器回调方法,通知日期时间已设置
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}

@ -0,0 +1,72 @@
/*
* 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.
*/
// DropdownMenu.java文件定义了一个下拉菜单相关的类
package net.micode.xiaomi_note.ui; // 定义包名,用于组织相关类
// 导入必要的Android框架类
import android.content.Context; // 提供应用环境信息的上下文类
import android.view.Menu; // 用于表示菜单的数据结构
import android.view.MenuItem; // 表示菜单中的具体选项
import android.view.View; // 基础视图类所有UI组件的基类
import android.view.View.OnClickListener; // 视图点击监听器接口
import android.widget.Button; // 按钮控件类
import android.widget.PopupMenu; // 弹出式菜单类
import android.widget.PopupMenu.OnMenuItemClickListener; // 弹出菜单项点击事件监听器接口
// 导入自定义资源文件
import net.micode.xiaomi_note.R; // 引入R类用于访问资源文件
// DropdownMenu类用于创建一个带有下拉菜单的功能按钮
public class DropdownMenu {
// 按钮对象,用于触发下拉菜单
private Button mButton;
// 弹出式菜单对象,用于显示下拉菜单
private PopupMenu mPopupMenu;
// 菜单对象,存储具体的菜单项
private Menu mMenu;
// 构造函数初始化DropdownMenu对象
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button; // 初始化按钮
mButton.setBackgroundResource(R.drawable.dropdown_icon); // 设置按钮背景图标
mPopupMenu = new PopupMenu(context, mButton); // 创建弹出式菜单
mMenu = mPopupMenu.getMenu(); // 获取菜单对象
mPopupMenu.getMenuInflater().inflate(menuId, mMenu); // 加载菜单资源到菜单对象
mButton.setOnClickListener(new OnClickListener() { // 为按钮设置点击监听器
public void onClick(View v) {
mPopupMenu.show(); // 显示下拉菜单
}
});
}
// 设置下拉菜单项点击事件的监听器
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener); // 绑定菜单项点击事件监听器
}
}
// 根据ID查找菜单项
public MenuItem findItem(int id) {
return mMenu.findItem(id); // 返回指定ID的菜单项
}
// 设置按钮标题
public void setTitle(CharSequence title) {
mButton.setText(title); // 设置按钮显示的标题
}
}

@ -0,0 +1,97 @@
/*
* 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.
*/
// FoldersListAdapter.java 文件
package net.micode.xiaomi_note.ui; // 定义包名,表示该文件属于 net.micode.xiaomi_note 包下的 ui 子包
// 导入必要的类
import android.content.Context; // 提供上下文环境,用于访问资源或服务
import android.database.Cursor; // 用于查询数据库结果集的操作
import android.view.View; // 基础视图类,所有 UI 组件的基类
import android.view.ViewGroup; // 视图容器的基类,用于布局管理
import android.widget.CursorAdapter; // 用于将 Cursor 数据绑定到 ListView 的适配器
import android.widget.LinearLayout; // 线性布局管理器
import android.widget.TextView; // 显示文本的控件
// 导入项目内部的资源和数据类
import net.micode.xiaomi_note.R; // 引入项目的资源文件
import net.micode.xiaomi_note.data.Notes; // 引入笔记相关的数据模型
import net.micode.xiaomi_note.data.Notes.NoteColumns; // 引入笔记表中的列定义
// 定义一个继承自CursorAdapter的适配器类用于管理文件夹列表的数据展示
public class FoldersListAdapter extends CursorAdapter {
// 定义查询投影,指定需要从数据库中获取的列
public static final String [] PROJECTION = {
NoteColumns.ID, // 文件夹ID列
NoteColumns.SNIPPET // 文件夹名称片段列
};
// 定义列索引常量,用于快速定位查询结果中的特定列
public static final int ID_COLUMN = 0; // ID列的索引
public static final int NAME_COLUMN = 1; // 名称片段列的索引
// 构造函数,初始化适配器
public FoldersListAdapter(Context context, Cursor c) {
super(context, c); // 调用父类构造函数,传入上下文和游标
// TODO: 需要实现具体的构造逻辑
}
// 创建一个新的视图项FolderListItem用于显示文件夹列表项
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context); // 返回一个新的FolderListItem实例
}
// 将数据绑定到已有的视图项上
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) { // 确保视图是FolderListItem类型
// 根据条件设置文件夹名称
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ?
context.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
// 调用FolderListItem的bind方法更新UI
((FolderListItem) view).bind(folderName);
}
}
// 获取指定位置的文件夹名称
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position); // 获取指定位置的游标
// 根据条件返回文件夹名称
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ?
context.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
// 内部类:定义文件夹列表项的视图组件
private class FolderListItem extends LinearLayout {
private TextView mName; // 文件夹名称的TextView控件
// 构造函数,初始化文件夹列表项视图
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this); // 加载布局文件
mName = (TextView) findViewById(R.id.tv_folder_name); // 查找TextView控件
}
// 绑定数据到视图上
public void bind(String name) {
mName.setText(name); // 设置文件夹名称文本
}
}
}

@ -0,0 +1,210 @@
/*
* 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.xiaomi_note.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 androidx.appcompat.widget.AppCompatEditText; // 兼容性 EditText 类
import net.micode.xiaomi_note.R; // 引入资源文件
import java.util.HashMap; // 哈希表数据结构
import java.util.Map; // 映射接口
public class NoteEditText extends AppCompatEditText {
private static final String TAG = "NoteEditText"; // 日志标签,用于调试信息输出
private int mIndex; // 当前编辑框的索引
private int mSelectionStartBeforeDelete; // 删除操作前的光标位置
private static final String SCHEME_TEL = "tel:" ; // 电话协议
private static final String SCHEME_HTTP = "http:" ; // HTTP协议
private static final String SCHEME_EMAIL = "mailto:" ; // 邮件协议
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>(); // 协议与资源ID映射表
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话协议对应的资源ID
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // HTTP协议对应的资源ID
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮件协议对应的资源ID
}
/**
* {@link NoteEditActivity}
*/
public interface OnTextViewChangeListener {
/**
*
*/
void onEditTextDelete(int index, String text);
/**
*
*/
void onEditTextEnter(int index, String text);
/**
*
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener; // 编辑框内容变化监听器
public NoteEditText(Context context) {
super(context); // 调用父类构造函数
mIndex = 0; // 初始化索引
}
public void setIndex(int index) { // 设置当前编辑框的索引
mIndex = index;
}
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) { // 设置内容变化监听器
mOnTextViewChangeListener = listener;
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle); // 调用父类构造函数
}
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); // 调用父类构造函数
// TODO: 自动生成的构造函数占位符
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) { // 处理触摸事件
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX(); // 获取触摸点的X坐标
int y = (int) event.getY(); // 获取触摸点的Y坐标
x -= getTotalPaddingLeft(); // 调整X坐标
y -= getTotalPaddingTop(); // 调整Y坐标
x += getScrollX(); // 添加滚动偏移
y += getScrollY(); // 添加滚动偏移
Layout layout = getLayout(); // 获取布局对象
int line = layout.getLineForVertical(y); // 获取点击行号
int off = layout.getOffsetForHorizontal(line, x); // 获取水平偏移对应的字符位置
Selection.setSelection(getText(), off); // 设置选中位置
break;
}
return super.onTouchEvent(event); // 调用父类方法处理事件
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) { // 处理按键事件
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
return false; // 不执行默认行为
}
break;
case KeyEvent.KEYCODE_DEL:
mSelectionStartBeforeDelete = getSelectionStart(); // 记录删除前的光标位置
break;
default:
break;
}
return super.onKeyDown(keyCode, event); // 调用父类方法处理事件
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) { // 处理按键抬起事件
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) { // 如果光标在首位且不是第一个编辑框
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString()); // 触发删除回调
return true; // 拦截事件
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted"); // 日志提示未设置监听器
}
break;
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart(); // 获取当前选中起始位置
String text = getText().subSequence(selectionStart, length()).toString(); // 截取后续文本
setText(getText().subSequence(0, selectionStart)); // 设置文本为选中前部分
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text); // 触发新增回调
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted"); // 日志提示未设置监听器
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event); // 调用父类方法处理事件
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) { // 如果失去焦点且文本为空
mOnTextViewChangeListener.onTextChange(mIndex, false); // 触发文本为空回调
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true); // 触发文本非空回调
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect); // 调用父类方法处理焦点变化
}
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) { // 如果文本是可扩展的
int selStart = getSelectionStart(); // 获取选区起点
int selEnd = getSelectionEnd(); // 获取选区终点
int min = Math.min(selStart, selEnd); // 计算最小值
int max = Math.max(selStart, selEnd); // 计算最大值
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class); // 获取选中的URLSpan
if (urls.length == 1) { // 如果选中一个URL
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) { // 遍历协议映射表
if(urls[0].getURL().indexOf(schema) >= 0) { // 如果URL包含该协议
defaultResId = sSchemaActionResMap.get(schema); // 获取对应的资源ID
break;
}
}
if (defaultResId == 0) { // 如果未找到匹配协议
defaultResId = R.string.note_link_other; // 使用默认资源ID
}
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener( // 添加上下文菜单项
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 跳转到新意图
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu); // 调用父类方法创建上下文菜单
}
}

@ -0,0 +1,311 @@
/*
* 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.xiaomi_note.ui; // 定义包名
// 导入必要的类和库
import android.content.Context; // 提供应用环境信息
import android.database.Cursor; // 数据库游标,用于查询结果
import android.text.TextUtils; // 提供文本操作工具
import net.micode.xiaomi_note.data.Contact; // 联系人数据模型
import net.micode.xiaomi_note.data.Notes; // 笔记数据模型
import net.micode.xiaomi_note.data.Notes.NoteColumns; // 笔记表的列定义
import net.micode.xiaomi_note.tool.DataUtils; // 工具类,提供通用方法
/**
*
*/
public class NoteItemData {
// 数据库查询字段投影数组,定义需要从数据库中查询的列
static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记唯一标识符
NoteColumns.ALERTED_DATE, // 提醒时间(时间戳)
NoteColumns.BG_COLOR_ID, // 背景颜色资源ID
NoteColumns.CREATED_DATE, // 创建时间(时间戳)
NoteColumns.HAS_ATTACHMENT, // 是否有附件0/1
NoteColumns.MODIFIED_DATE, // 最后修改时间(时间戳)
NoteColumns.NOTES_COUNT, // 包含的子笔记数量(用于文件夹)
NoteColumns.PARENT_ID, // 父笔记ID用于构建层级结构
NoteColumns.SNIPPET, // 内容摘要(显示在列表中的预览文本)
NoteColumns.TYPE, // 笔记类型(普通笔记/文件夹/系统笔记等)
NoteColumns.WIDGET_ID, // 关联的小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型2x1,4x2等
};
// 列索引常量对应PROJECTION数组中的位置
private static final int ID_COLUMN = 0; // ID字段在游标中的位置
private static final int ALERTED_DATE_COLUMN = 1; // 提醒日期字段位置
private static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色字段位置
private static final int CREATED_DATE_COLUMN = 3; // 创建时间字段位置
private static final int HAS_ATTACHMENT_COLUMN= 4; // 附件标记字段位置
private static final int MODIFIED_DATE_COLUMN = 5; // 修改时间字段位置
private static final int NOTES_COUNT_COLUMN = 6; // 笔记数量字段位置
private static final int PARENT_ID_COLUMN = 7; // 父ID字段位置
private static final int SNIPPET_COLUMN = 8; // 内容摘要字段位置
private static final int TYPE_COLUMN = 9; // 类型字段位置
private static final int WIDGET_ID_COLUMN = 10; // 小部件ID字段位置
private static final int WIDGET_TYPE_COLUMN = 11; // 小部件类型字段位置
// 笔记数据字段
private long mId; // 笔记唯一ID对应数据库主键
private long mAlertDate; // 提醒时间0表示未设置提醒
private int mBgColorId; // 背景颜色资源ID
private long mCreatedDate; // 创建时间戳(毫秒)
private boolean mHasAttachment; // 是否有附件(图片/音频等)
private long mModifiedDate; // 最后修改时间戳
private int mNotesCount; // 子项数量(当这是文件夹时有效)
private long mParentId; // 父笔记ID用于构建树形结构
private String mSnippet; // 内容预览(处理后去掉了复选框标记)
private int mType; // 笔记类型见Notes.TYPE_*常量)
private int mWidgetId; // 关联的小部件ID0表示未关联
private int mWidgetType; // 小部件类型见Notes.TYPE_WIDGET_*
// 联系人相关字段(当笔记是通话记录时使用)
private String mName; // 联系人姓名(通话记录专用)
private String mPhoneNumber; // 电话号码(通话记录专用)
// 列表位置状态标记用于UI渲染
private boolean mIsLastItem; // 是否是列表中的最后一项
private boolean mIsFirstItem; // 是否是列表中的第一项
private boolean mIsOnlyOneItem; // 是否是列表中的唯一项
private boolean mIsOneNoteFollowingFolder; // 是否是一个笔记紧跟在文件夹后面
private boolean mIsMultiNotesFollowingFolder; // 是否是多个笔记紧跟在文件夹后面
public NoteItemData(Context context, Cursor cursor) {
// 获取数据库游标中的ID列值并赋值给成员变量mId
mId = cursor.getLong(ID_COLUMN);
// 获取数据库游标中的提醒日期列值并赋值给成员变量mAlertDate
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
// 获取数据库游标中的背景颜色ID列值并赋值给成员变量mBgColorId
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
// 获取数据库游标中的创建日期列值并赋值给成员变量mCreatedDate
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
// 判断是否有附件若有则将成员变量mHasAttachment设为true
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
// 获取数据库游标中的修改日期列值并赋值给成员变量mModifiedDate
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
// 获取数据库游标中的笔记数量列值并赋值给成员变量mNotesCount
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
// 获取数据库游标中的父ID列值并赋值给成员变量mParentId
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 获取数据库游标中的摘要列值并赋值给成员变量mSnippet
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 去除摘要中的特定标签字符
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
// 获取数据库游标中的类型列值并赋值给成员变量mType
mType = cursor.getInt(TYPE_COLUMN);
// 获取数据库游标中的小部件ID列值并赋值给成员变量mWidgetId
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
// 获取数据库游标中的小部件类型列值并赋值给成员变量mWidgetType
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 初始化电话号码为空字符串
mPhoneNumber = "";
// 如果父ID等于通话记录文件夹ID则执行相关操作
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 获取与笔记ID关联的通话号码
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
// 如果电话号码不为空,则尝试获取联系人名称
if (!TextUtils.isEmpty(mPhoneNumber)) {
mName = Contact.getContact(context, mPhoneNumber);
// 如果联系人名称为空,则使用电话号码作为名称
if (mName == null) {
mName = mPhoneNumber;
}
}
}
// 如果联系人名称仍为空,则初始化为空字符串
if (mName == null) {
mName = "";
}
// 调用检查位置的方法
checkPostion(cursor);
}
// 检查当前游标的位置状态
private void checkPostion(Cursor cursor) {
// 判断当前游标是否是最后一个元素并赋值给成员变量mIsLastItem
mIsLastItem = cursor.isLast() ? true : false;
// 判断当前游标是否是第一个元素并赋值给成员变量mIsFirstItem
mIsFirstItem = cursor.isFirst() ? true : false;
// 判断当前游标是否只有一个元素并赋值给成员变量mIsOnlyOneItem
mIsOnlyOneItem = (cursor.getCount() == 1);
// 初始化多条笔记后跟文件夹标志位
mIsMultiNotesFollowingFolder = false;
// 初始化一条笔记后跟文件夹标志位
mIsOneNoteFollowingFolder = false;
// 如果当前笔记类型为普通笔记且不是第一个元素
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
// 获取当前游标的位置
int position = cursor.getPosition();
// 将游标移动到前一个位置
if (cursor.moveToPrevious()) {
// 如果前一个元素类型为文件夹或系统类型
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 如果游标之后还有更多元素,则设置多条笔记后跟文件夹标志位
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
// 否则设置一条笔记后跟文件夹标志位
mIsOneNoteFollowingFolder = true;
}
}
// 尝试将游标移回原位置,若失败则抛出异常
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
public boolean isOneFollowingFolder() {
// 判断当前项是否是仅跟随一个文件夹的标记
return mIsOneNoteFollowingFolder;
}
public boolean isMultiFollowingFolder() {
// 判断当前项是否是跟随多个文件夹的标记
return mIsMultiNotesFollowingFolder;
}
public boolean isLast() {
// 判断当前项是否是最后一个项目的标记
return mIsLastItem;
}
public String getCallName() {
// 获取当前项的呼叫名称
return mName;
}
public boolean isFirst() {
// 判断当前项是否是第一个项目的标记
return mIsFirstItem;
}
public boolean isSingle() {
// 判断当前项是否是唯一一项的标记
return mIsOnlyOneItem;
}
public long getId() {
// 获取当前项的唯一标识符ID
return mId;
}
public long getAlertDate() {
// 获取当前项的提醒日期
return mAlertDate;
}
public long getCreatedDate() {
// 获取当前项的创建日期
return mCreatedDate;
}
public boolean hasAttachment() {
// 判断当前项是否有附件的标记
return mHasAttachment;
}
public long getModifiedDate() {
// 获取当前项的最后修改日期
return mModifiedDate;
}
public int getBgColorId() {
// 获取当前项的背景颜色ID
return mBgColorId;
}
public long getParentId() {
// 获取当前项的父级ID
return mParentId;
}
public int getNotesCount() {
// 获取当前项关联的笔记数量
return mNotesCount;
}
public long getFolderId () {
// 获取当前项所属文件夹的ID与getParentId方法相同
return mParentId;
}
public int getType() {
// 获取当前项的类型
return mType;
}
public int getWidgetType() {
// 获取当前项的小部件类型
return mWidgetType;
}
public int getWidgetId() {
// 获取当前项的小部件ID
return mWidgetId;
}
public String getSnippet() {
// 获取当前项的摘要信息
return mSnippet;
}
public boolean hasAlert() {
// 判断当前项是否有设置提醒的标记
return (mAlertDate > 0);
}
public boolean isCallRecord() {
// 判断当前项是否是通话记录的标记
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
public static int getNoteType(Cursor cursor) {
// 从游标中获取笔记类型的整数值
return cursor.getInt(TYPE_COLUMN);
}
}

@ -0,0 +1,254 @@
/*
* 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.xiaomi_note.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.xiaomi_note.data.Notes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
* CursorAdapter
*
*/
public class NotesListAdapter extends CursorAdapter {
private static final String TAG = "NotesListAdapter"; // 日志标签
private Context mContext; // 上下文对象
private HashMap<Integer, Boolean> mSelectedIndex; // 存储选中项的索引和状态
private int mNotesCount; // 笔记总数
private boolean mChoiceMode; // 是否处于选择模式
/**
* ID
*/
public static class AppWidgetAttribute {
public int widgetId; // 小部件ID
public int widgetType; // 小部件类型
};
/**
*
* @param context
*/
public NotesListAdapter(Context context) {
super(context, null); // 调用父类构造函数
mSelectedIndex = new HashMap<Integer, Boolean>(); // 初始化选中索引映射表
mContext = context; // 设置上下文
mNotesCount = 0; // 初始化笔记总数为0
}
/**
*
* @param context
* @param cursor
* @param parent
* @return
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context); // 返回一个新实例化的NotesListItem
}
/**
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) { // 检查视图是否为NotesListItem类型
NoteItemData itemData = new NoteItemData(context, cursor); // 创建NoteItemData对象
((NotesListItem) view).bind(context, itemData, mChoiceMode, // 绑定数据到视图
isSelectedItem(cursor.getPosition()));
}
}
/**
*
* @param position
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked); // 更新选中状态
notifyDataSetChanged(); // 通知数据集已改变
}
/**
*
* @return
*/
public boolean isInChoiceMode() {
return mChoiceMode; // 返回选择模式标志
}
/**
*
* @param mode
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear(); // 清空选中状态
mChoiceMode = mode; // 设置选择模式标志
}
/**
*
* @param checked
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor(); // 获取数据游标
for (int i = 0; i < getCount(); i++) { // 遍历所有数据项
if (cursor.moveToPosition(i)) { // 移动到指定位置
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { // 判断是否为笔记类型
setCheckedItem(i, checked); // 设置选中状态
}
}
}
}
/**
* ID
* @return ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>(); // 初始化结果集合
for (Integer position : mSelectedIndex.keySet()) { // 遍历选中索引
if (mSelectedIndex.get(position) == true) { // 如果选中
Long id = getItemId(position); // 获取项目ID
if (id == Notes.ID_ROOT_FOLDER) { // 如果ID为根文件夹ID
Log.d(TAG, "Wrong item id, should not happen"); // 记录日志
} else {
itemSet.add(id); // 添加到结果集合
}
}
}
return itemSet; // 返回结果集合
}
/**
*
* @return
*/
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); // 创建NoteItemData对象
widget.widgetId = item.getWidgetId(); // 设置小部件ID
widget.widgetType = item.getWidgetType(); // 设置小部件类型
itemSet.add(widget); // 添加到结果集合
/**
*
*/
} else {
Log.e(TAG, "Invalid cursor"); // 记录错误日志
return null; // 返回空值
}
}
}
return itemSet; // 返回结果集合
}
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values(); // 获取选中状态集合
if (null == values) { // 如果集合为空
return 0; // 返回0
}
Iterator<Boolean> iter = values.iterator(); // 获取迭代器
int count = 0; // 初始化计数器
while (iter.hasNext()) { // 遍历集合
if (true == iter.next()) { // 如果选中
count++; // 增加计数器
}
}
return count; // 返回选中数量
}
/**
*
* @return
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount(); // 获取选中数量
return (checkedCount != 0 && checkedCount == mNotesCount); // 判断是否等于总数量
}
/**
*
* @param position
* @return
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) { // 如果索引不存在
return false; // 返回未选中
}
return mSelectedIndex.get(position); // 返回选中状态
}
/**
*
*/
@Override
protected void onContentChanged() {
super.onContentChanged(); // 调用父类方法
calcNotesCount(); // 计算笔记总数
}
/**
*
* @param cursor
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor); // 调用父类方法
calcNotesCount(); // 计算笔记总数
}
/**
*
*/
private void calcNotesCount() {
mNotesCount = 0; // 初始化笔记总数为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,107 @@
// NotesListItem.java 文件定义了一个自定义的 LinearLayout用于显示笔记列表项。
package net.micode.xiaomi_note.ui;
import android.content.Context; // 导入 Context 类,用于获取应用上下文信息
import android.text.format.DateUtils; // 导入 DateUtils 工具类,用于格式化时间
import android.view.View; // 导入 View 类,用于操作视图组件
import android.widget.CheckBox; // 导入 CheckBox用于多选模式下的复选框
import android.widget.ImageView; // 导入 ImageView用于显示图标
import android.widget.LinearLayout; // 导入 LinearLayout作为该类的基类
import android.widget.TextView; // 导入 TextView用于显示文本
import net.micode.xiaomi_note.R; // 导入资源文件中的常量
import net.micode.xiaomi_note.data.Notes; // 导入 Notes 数据模型
import net.micode.xiaomi_note.tool.DataUtils; // 导入工具类 DataUtils
import net.micode.xiaomi_note.tool.ResourceParser.NoteItemBgResources; // 导入背景资源解析器
public class NotesListItem extends LinearLayout { // 定义一个继承自 LinearLayout 的自定义视图类
private ImageView mAlert; // 声明一个 ImageView用于显示提醒图标
private TextView mTitle; // 声明一个 TextView用于显示标题
private TextView mTime; // 声明一个 TextView用于显示时间
private TextView mCallName; // 声明一个 TextView用于显示通话名称
private NoteItemData mItemData; // 声明一个 NoteItemData 对象,存储当前列表项的数据
private CheckBox mCheckBox; // 声明一个 CheckBox用于多选模式下的复选框
public NotesListItem(Context context) { // 构造函数,初始化视图
super(context); // 调用父类构造函数
inflate(context, R.layout.note_item, this); // 使用布局文件 note_item 初始化视图
mAlert = (ImageView) findViewById(R.id.iv_alert_icon); // 获取提醒图标控件
mTitle = (TextView) findViewById(R.id.tv_title); // 获取标题控件
mTime = (TextView) findViewById(R.id.tv_time); // 获取时间控件
mCallName = (TextView) findViewById(R.id.tv_name); // 获取通话名称控件
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox); // 获取复选框控件
}
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { // 绑定数据到视图
if (choiceMode && data.getType() == Notes.TYPE_NOTE) { // 如果处于选择模式且类型为笔记
mCheckBox.setVisibility(View.VISIBLE); // 显示复选框
mCheckBox.setChecked(checked); // 设置复选框状态
} else {
mCheckBox.setVisibility(View.GONE); // 否则隐藏复选框
}
mItemData = data; // 存储当前数据对象
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) { // 如果是通话记录文件夹
mCallName.setVisibility(View.GONE); // 隐藏通话名称
mAlert.setVisibility(View.VISIBLE); // 显示提醒图标
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题样式
mTitle.setText(context.getString(R.string.call_record_folder_name) // 设置标题文本
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record); // 设置提醒图标资源
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) { // 如果是通话记录子项
mCallName.setVisibility(View.VISIBLE); // 显示通话名称
mCallName.setText(data.getCallName()); // 设置通话名称文本
mTitle.setTextAppearance(context, R.style.TextAppearanceSecondaryItem); // 设置标题样式
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本
if (data.hasAlert()) { // 如果有提醒
mAlert.setImageResource(R.drawable.clock); // 设置提醒图标资源
mAlert.setVisibility(View.VISIBLE); // 显示提醒图标
} else {
mAlert.setVisibility(View.GONE); // 否则隐藏提醒图标
}
} else { // 其他情况(普通笔记或文件夹)
mCallName.setVisibility(View.GONE); // 隐藏通话名称
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置标题样式
if (data.getType() == Notes.TYPE_FOLDER) { // 如果是文件夹
mTitle.setText(data.getSnippet() // 设置标题文本
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE); // 隐藏提醒图标
} else { // 如果是普通笔记
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet())); // 设置标题文本
if (data.hasAlert()) { // 如果有提醒
mAlert.setImageResource(R.drawable.clock); // 设置提醒图标资源
mAlert.setVisibility(View.VISIBLE); // 显示提醒图标
} else {
mAlert.setVisibility(View.GONE); // 否则隐藏提醒图标
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); // 设置时间文本
setBackground(data); // 设置背景
}
private void setBackground(NoteItemData data) { // 设置背景资源
int id = data.getBgColorId(); // 获取背景颜色 ID
if (data.getType() == Notes.TYPE_NOTE) { // 如果是笔记
if (data.isSingle() || data.isOneFollowingFolder()) { // 如果是单个笔记或跟随文件夹的第一个
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id)); // 设置单个笔记背景
} else if (data.isLast()) { // 如果是最后一个笔记
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id)); // 设置最后一个笔记背景
} else if (data.isFirst() || data.isMultiFollowingFolder()) { // 如果是第一个笔记或多个跟随文件夹
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id)); // 设置第一个笔记背景
} else { // 其他情况
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id)); // 设置普通笔记背景
}
} else { // 如果是文件夹
setBackgroundResource(NoteItemBgResources.getFolderBgRes()); // 设置文件夹背景
}
}
public NoteItemData getItemData() { // 获取当前数据对象
return mItemData; // 返回数据对象
}
}

@ -0,0 +1,376 @@
/*
* 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.xiaomi_note.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.xiaomi_note.R;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.gtask.remote.GTaskSyncService;
// 定义偏好设置活动类
public class NotesPreferenceActivity extends PreferenceActivity {
public static final String PREFERENCE_NAME = "notes_preferences"; // 共享偏好文件名
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name"; // 同步账户名称键
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time"; // 最后同步时间键
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; // 背景颜色随机出现键
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; // 同步账户键
private static final String AUTHORITIES_FILTER_KEY = "authorities"; // 权限过滤键
private PreferenceCategory mAccountCategory; // 账户类别偏好
private GTaskReceiver mReceiver; // 广播接收器
private Account[] mOriAccounts; // 原始账户数组
private boolean mHasAddedAccount; // 是否已添加账户标志
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* 使用应用图标作为导航 */
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); // 设置广播动作
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED); // 注册广播接收器API 33及以上
} else {
registerReceiver(mReceiver, filter); // 注册广播接收器API 33以下
}
mOriAccounts = null; // 初始化原始账户数组
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); // 加载设置头部布局
getListView().addHeaderView(header, null, true); // 添加头部视图到列表视图
}
@Override
protected void onResume() {
super.onResume();
// 如果用户已添加新账户,则自动设置同步账户
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts(); // 获取所有Google账户
if (mOriAccounts != null && accounts.length > mOriAccounts.length) { // 检查是否有新增账户
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) { // 比较账户名
found = true;
break;
}
}
if (!found) { // 如果未找到匹配账户
setSyncAccount(accountNew.name); // 设置同步账户
break;
}
}
}
}
refreshUI(); // 刷新用户界面
}
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver); // 注销广播接收器
}
super.onDestroy();
}
private void loadAccountPreference() {
mAccountCategory.removeAll(); // 清空账户类别中的所有偏好
Preference accountPref = new Preference(this); // 创建新的偏好项
final String defaultAccount = getSyncAccountName(this); // 获取默认同步账户名
accountPref.setTitle(getString(R.string.preferences_account_title)); // 设置标题
accountPref.setSummary(getString(R.string.preferences_account_summary)); // 设置摘要
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { // 设置点击监听器
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) { // 如果未处于同步状态
if (TextUtils.isEmpty(defaultAccount)) { // 如果默认账户为空
showSelectAccountAlertDialog(); // 显示选择账户对话框
} else {
showChangeAccountConfirmAlertDialog(); // 显示更改账户确认对话框
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show(); // 显示无法更改账户的提示
}
return true;
}
});
mAccountCategory.addPreference(accountPref); // 将账户偏好添加到类别中
}
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button); // 获取同步按钮
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview); // 获取最后同步时间文本视图
// 根据同步状态设置按钮文本和点击事件
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel)); // 设置取消同步文本
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步
}
});
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately)); // 设置立即同步文本
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this); // 开始同步
}
});
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this))); // 禁用无账户时的同步按钮
// 设置最后同步时间
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString()); // 设置同步进度文本
lastSyncTimeView.setVisibility(View.VISIBLE); // 显示同步进度视图
} else {
long lastSyncTime = getLastSyncTime(this); // 获取最后同步时间
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime))); // 设置最后同步时间文本
lastSyncTimeView.setVisibility(View.VISIBLE); // 显示最后同步时间视图
} else {
lastSyncTimeView.setVisibility(View.GONE); // 隐藏最后同步时间视图
}
}
}
private void refreshUI() {
loadAccountPreference(); // 刷新账户偏好
loadSyncButton(); // 刷新同步按钮
}
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); // 创建对话框构建器
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); // 加载对话框标题布局
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); // 获取标题文本视图
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title)); // 设置标题文本
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); // 获取副标题文本视图
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips)); // 设置副标题文本
dialogBuilder.setCustomTitle(titleView); // 设置自定义标题
dialogBuilder.setPositiveButton(null, null); // 移除默认的确定按钮
Account[] accounts = getGoogleAccounts(); // 获取所有Google账户
String defAccount = getSyncAccountName(this); // 获取默认同步账户名
mOriAccounts = accounts; // 更新原始账户数组
mHasAddedAccount = false; // 重置账户添加标志
if (accounts.length > 0) { // 如果存在账户
CharSequence[] items = new CharSequence[accounts.length]; // 创建选项数组
final CharSequence[] itemMapping = items; // 保存选项映射
int checkedItem = -1; // 默认未选中
int index = 0; // 当前索引
for (Account account : accounts) { // 遍历账户
if (TextUtils.equals(account.name, defAccount)) { // 如果账户名匹配默认账户
checkedItem = index; // 设置选中项
}
items[index++] = account.name; // 添加账户名到选项数组
}
dialogBuilder.setSingleChoiceItems(items, checkedItem, // 设置单选列表
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString()); // 设置同步账户
dialog.dismiss(); // 关闭对话框
refreshUI(); // 刷新用户界面
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null); // 加载添加账户视图
dialogBuilder.setView(addAccountView); // 设置视图
final AlertDialog dialog = dialogBuilder.show(); // 显示对话框
addAccountView.setOnClickListener(new View.OnClickListener() { // 设置添加账户点击事件
public void onClick(View v) {
mHasAddedAccount = true; // 标记已添加账户
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); // 创建添加账户意图
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { "gmail-ls" }); // 设置权限过滤器
startActivityForResult(intent, -1); // 启动添加账户活动
dialog.dismiss(); // 关闭对话框
}
});
}
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); // 创建对话框构建器
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); // 加载对话框标题布局
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); // 获取标题文本视图
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this))); // 设置标题文本
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); // 获取副标题文本视图
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); // 设置副标题文本
dialogBuilder.setCustomTitle(titleView); // 设置自定义标题
CharSequence[] menuItemArray = new CharSequence[] { // 定义菜单项数组
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { // 设置菜单项点击事件
public void onClick(DialogInterface dialog, int which) {
if (which == 0) { // 如果选择更改账户
showSelectAccountAlertDialog(); // 显示选择账户对话框
} else if (which == 1) { // 如果选择移除账户
removeSyncAccount(); // 移除同步账户
refreshUI(); // 刷新用户界面
}
}
});
dialogBuilder.show(); // 显示对话框
}
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this); // 获取账户管理器
return accountManager.getAccountsByType("com.google"); // 获取所有Google账户
}
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) { // 如果账户不同
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取共享偏好
SharedPreferences.Editor editor = settings.edit(); // 获取编辑器
if (account != null) { // 如果账户不为空
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 保存账户名
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 清空账户名
}
editor.commit(); // 提交更改
// 清理最后同步时间
setLastSyncTime(this, 0);
// 清理本地任务相关数据
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues(); // 创建内容值对象
values.put(NoteColumns.GTASK_ID, ""); // 清空任务ID
values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); // 更新数据库
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account), // 显示成功设置账户的提示
Toast.LENGTH_SHORT).show();
}
}
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取共享偏好
SharedPreferences.Editor editor = settings.edit(); // 获取编辑器
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { // 如果包含同步账户名
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); // 移除同步账户名
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { // 如果包含最后同步时间
editor.remove(PREFERENCE_LAST_SYNC_TIME); // 移除最后同步时间
}
editor.commit(); // 提交更改
// 清理本地任务相关数据
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues(); // 创建内容值对象
values.put(NoteColumns.GTASK_ID, ""); // 清空任务ID
values.put(NoteColumns.SYNC_ID, 0); // 清空同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); // 更新数据库
}
}).start();
}
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取共享偏好
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 返回同步账户名
}
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取共享偏好
SharedPreferences.Editor editor = settings.edit(); // 获取编辑器
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time); // 保存最后同步时间
editor.commit(); // 提交更改
}
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取共享偏好
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); // 返回最后同步时间
}
private class GTaskReceiver extends BroadcastReceiver { // 广播接收器内部类
@Override
public void onReceive(Context context, Intent intent) {
refreshUI(); // 刷新用户界面
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { // 如果正在同步
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); // 获取同步状态文本视图
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG)); // 设置同步进度文本
}
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: // 如果点击返回按钮
Intent intent = new Intent(this, NotesListActivity.class); // 创建笔记列表意图
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 设置清除顶部标志
startActivity(intent); // 启动笔记列表活动
return true;
default:
return false;
}
}
}

@ -0,0 +1,142 @@
package net.micode.xiaomi_note.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.xiaomi_note.R;
import net.micode.xiaomi_note.data.Notes;
import net.micode.xiaomi_note.data.Notes.NoteColumns;
import net.micode.xiaomi_note.tool.ResourceParser;
import net.micode.xiaomi_note.ui.NoteEditActivity;
import net.micode.xiaomi_note.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; // 笔记ID列索引
public static final int COLUMN_BG_COLOR_ID = 1; // 背景颜色ID列索引
public static final int COLUMN_SNIPPET = 2; // 笔记片段列索引
private static final String TAG = "NoteWidgetProvider"; // 日志标签
/**
* ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
ContentValues values = new ContentValues(); // 用于存储要更新的数据
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); // 设置无效的小部件ID
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI, // 更新笔记内容URI
values,
NoteColumns.WIDGET_ID + "=?", // 条件小部件ID匹配
new String[] { String.valueOf(appWidgetIds[i])}); // 参数当前小部件ID
}
}
/**
* ID
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 查询笔记内容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) { // 检查小部件ID是否有效
int bgId = ResourceParser.getDefaultBgId(context); // 获取默认背景ID
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]); // 添加小部件ID
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); // 获取背景颜色ID
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); // 添加笔记ID
intent.setAction(Intent.ACTION_VIEW); // 设置意图动作
} else { // 如果没有查询到笔记信息
snippet = context.getResources().getString(R.string.widget_havenot_content); // 设置默认文本
intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 设置意图动作
}
if (c != null) {
c.close(); // 关闭游标
}
// 创建远程视图对象
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); // 设置背景图片
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); // 添加背景ID到意图
/**
* PendingIntent
*/
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); // 创建跳转到笔记列表的PendingIntent
} else { // 非隐私模式
rv.setTextViewText(R.id.widget_text, snippet); // 设置笔记片段文本
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT); // 创建跳转到笔记编辑页面的PendingIntent
}
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); // 将PendingIntent绑定到文本视图
appWidgetManager.updateAppWidget(appWidgetIds[i], rv); // 更新小部件视图
}
}
}
/**
* ID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
*/
protected abstract int getLayoutId();
/**
*
*/
protected abstract int getWidgetType();
}

@ -0,0 +1,39 @@
// NoteWidgetProvider_2x.java 文件
// 该类继承自 NoteWidgetProvider用于处理 2x 尺寸的小米便签小组件的功能。
package net.micode.xiaomi_note.widget; // 定义包名,存放与小米便签小组件相关的类
// 导入必要的 Android 和项目内部的类
import android.appwidget.AppWidgetManager; // 提供对 App Widget 的管理功能
import android.content.Context; // 提供对应用程序上下文的操作
// 导入项目内部资源和工具类
import net.micode.xiaomi_note.R; // 包含应用的资源 ID
import net.micode.xiaomi_note.data.Notes; // 包含便签数据模型
import net.micode.xiaomi_note.tool.ResourceParser; // 提供资源解析功能
public class NoteWidgetProvider_2x extends NoteWidgetProvider { // 定义 2x 尺寸的便签小组件提供者
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 覆写 onUpdate 方法,当小组件被更新时调用
super.update(context, appWidgetManager, appWidgetIds); // 调用父类方法执行通用更新逻辑
}
@Override
protected int getLayoutId() {
// 覆写获取布局 ID 的方法,返回 2x 尺寸的布局文件 ID
return R.layout.widget_2x; // 返回 2x 尺寸的布局资源 ID
}
@Override
protected int getBgResourceId(int bgId) {
// 覆写获取背景资源 ID 的方法,根据传入的背景 ID 获取对应的 2x 尺寸背景资源
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); // 调用工具类方法解析背景资源
}
@Override
protected int getWidgetType() {
// 覆写获取小组件类型的抽象方法,返回 2x 尺寸的便签类型
return Notes.TYPE_WIDGET_2X; // 返回 2x 尺寸的便签类型常量
}
}

@ -0,0 +1,38 @@
// NoteWidgetProvider_4x.java 文件
// 该类继承自 NoteWidgetProvider用于处理 4x 尺寸的小米便签小组件的功能。
package net.micode.xiaomi_note.widget; // 定义包名,存放与小米便签小组件相关的类
// 导入必要的 Android 和自定义类
import android.appwidget.AppWidgetManager; // 提供对 App Widget 的管理功能
import android.content.Context; // 提供应用程序上下文信息
// 导入项目中定义的资源和工具类
import net.micode.xiaomi_note.R; // 包含应用的资源 ID
import net.micode.xiaomi_note.data.Notes; // 包含便签数据模型
import net.micode.xiaomi_note.tool.ResourceParser; // 提供资源解析功能
public class NoteWidgetProvider_4x extends NoteWidgetProvider { // 定义一个继承自 NoteWidgetProvider 的类
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 重写 onUpdate 方法,当小组件被更新时触发
super.update(context, appWidgetManager, appWidgetIds); // 调用父类的更新逻辑
}
protected int getLayoutId() {
// 返回 4x 尺寸小组件的布局资源 ID
return R.layout.widget_4x; // 返回值为 widget_4x 布局文件的资源 ID
}
@Override
protected int getBgResourceId(int bgId) {
// 根据背景 ID 获取对应的 4x 尺寸小组件背景资源 ID
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); // 调用工具类方法解析背景资源
}
@Override
protected int getWidgetType() {
// 返回当前小组件的类型,表示这是一个 4x 尺寸的便签小组件
return Notes.TYPE_WIDGET_4X; // 返回常量值,标识小组件类型
}
}

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

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

Loading…
Cancel
Save