Compare commits

..

No commits in common. 'master' and 'develop' have entirely different histories.

@ -1,168 +0,0 @@
<?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="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<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.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NotesMaster"
tools:targetApi="31">
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/Theme.NotesMaster"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/Theme.NotesMaster"
android:exported="true">
<intent-filter >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter >
<intent-filter >
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_2x_info" />
</receiver>
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_4x_info" />
</receiver>
<receiver android:name=".ui.AlarmInitReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@style/Theme.NotesMaster" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@style/Theme.NotesMaster" >
</activity>
<activity
android:name="net.micode.notes.ui.LoginActivity"
android:label="@string/title_login"
android:launchMode="singleTop"
android:theme="@style/Theme.NotesMaster" >
</activity>
<activity
android:name="net.micode.notes.ui.RegisterActivity"
android:label="@string/title_register"
android:launchMode="singleTop"
android:theme="@style/Theme.NotesMaster" >
</activity>
<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>

@ -1,78 +0,0 @@
package net.micode.notes;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.os.StrictMode;
import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import net.micode.notes.data.NotesDatabaseHelper;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 启用StrictMode检测UI线程的磁盘操作
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.penaltyLog()
.build());
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
private void checkDatabase() {
new Thread(() -> {
try {
NotesDatabaseHelper dbHelper = NotesDatabaseHelper.getInstance(this);
SQLiteDatabase db = dbHelper.getReadableDatabase();
// 检查表是否存在
Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'",
null);
boolean tableExists = cursor != null && cursor.getCount() > 0;
Log.d("Debug", "附件表存在: " + tableExists);
if (cursor != null) cursor.close();
// 查询附件数据
cursor = db.rawQuery("SELECT COUNT(*) FROM note_attachment", null);
if (cursor != null && cursor.moveToFirst()) {
int count = cursor.getInt(0);
Log.d("Debug", "附件表记录数: " + count);
}
if (cursor != null) cursor.close();
// 列出所有附件
cursor = db.query("note_attachment", null, null, null, null, null, null);
if (cursor != null) {
Log.d("Debug", "附件详情 - 总数: " + cursor.getCount());
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
long noteId = cursor.getLong(cursor.getColumnIndexOrThrow("note_id"));
String path = cursor.getString(cursor.getColumnIndexOrThrow("file_path"));
Log.d("Debug", String.format("附件: id=%d, noteId=%d, path=%s", id, noteId, path));
}
cursor.close();
}
} catch (Exception e) {
Log.e("Debug", "数据库检查失败", e);
}
}).start();
}
}

@ -1,214 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.account;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
*
*
*
* : SharedPreferences
* ,使
* 使 Android Keystore
*/
public class AccountManager {
private static final String TAG = "AccountManager";
private static final String PREF_NAME = "notes_preferences";
// 用户登录状态
private static final String PREF_USER_LOGGED_IN = "pref_user_logged_in";
// 当前登录用户名
private static final String PREF_CURRENT_USERNAME = "pref_current_username";
// 用户数据前缀 (用于存储多个用户)
private static final String PREF_USER_DATA_PREFIX = "pref_user_data_";
// 用户密码后缀 (格式: pref_user_data_<username>_password)
private static final String PREF_USER_PASSWORD_SUFFIX = "_password";
/**
*
* @param context
* @return true,false
*/
public static boolean isUserLoggedIn(Context context) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return sp.getBoolean(PREF_USER_LOGGED_IN, false);
} catch (Exception e) {
Log.e(TAG, "Error checking login status", e);
return false;
}
}
/**
*
* @param context
* @return ,
*/
public static String getCurrentUser(Context context) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return sp.getString(PREF_CURRENT_USERNAME, "");
} catch (Exception e) {
Log.e(TAG, "Error getting current user", e);
return "";
}
}
/**
*
* @param context
* @param username
* @param password
* @return true,false
*/
public static boolean login(Context context, String username, String password) {
if (username == null || username.isEmpty()) {
Log.w(TAG, "Empty username provided");
return false;
}
if (password == null || password.isEmpty()) {
Log.w(TAG, "Empty password provided");
return false;
}
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String storedPassword = sp.getString(getUserPasswordKey(username), "");
if (storedPassword.equals(password)) {
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean(PREF_USER_LOGGED_IN, true);
editor.putString(PREF_CURRENT_USERNAME, username);
boolean result = editor.commit();
if (result) {
Log.d(TAG, "User logged in successfully: " + username);
} else {
Log.e(TAG, "Failed to save login status");
}
return result;
} else {
Log.w(TAG, "Login failed: incorrect password for user " + username);
return false;
}
} catch (Exception e) {
Log.e(TAG, "Error during login", e);
return false;
}
}
/**
*
* @param context
* @param username
* @param password
* @return true,false
*/
public static boolean register(Context context, String username, String password) {
if (username == null || username.isEmpty()) {
Log.w(TAG, "Empty username provided");
return false;
}
if (password == null || password.isEmpty()) {
Log.w(TAG, "Empty password provided");
return false;
}
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String userPasswordKey = getUserPasswordKey(username);
if (sp.contains(userPasswordKey)) {
Log.w(TAG, "Registration failed: user already exists - " + username);
return false;
}
SharedPreferences.Editor editor = sp.edit();
editor.putString(userPasswordKey, password);
boolean result = editor.commit();
if (result) {
Log.d(TAG, "User registered successfully: " + username);
} else {
Log.e(TAG, "Failed to register user");
}
return result;
} catch (Exception e) {
Log.e(TAG, "Error during registration", e);
return false;
}
}
/**
*
* @param context
* @return true,false
*/
public static boolean logout(Context context) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean(PREF_USER_LOGGED_IN, false);
editor.putString(PREF_CURRENT_USERNAME, "");
boolean result = editor.commit();
if (result) {
Log.d(TAG, "User logged out successfully");
} else {
Log.e(TAG, "Failed to logout");
}
return result;
} catch (Exception e) {
Log.e(TAG, "Error during logout", e);
return false;
}
}
/**
*
* @param context
* @param username
* @return true,false
*/
public static boolean isUserExists(Context context, String username) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return sp.contains(getUserPasswordKey(username));
} catch (Exception e) {
Log.e(TAG, "Error checking if user exists", e);
return false;
}
}
/**
* Key
* @param username
* @return Key
*/
private static String getUserPasswordKey(String username) {
return PREF_USER_DATA_PREFIX + username + PREF_USER_PASSWORD_SUFFIX;
}
}

@ -1,63 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.cloud;
/**
* OSSConfig - OSS
*
*
*
*
* 1. https://oss.console.aliyun.com/
* 2. OSS Bucket
* 3. "概览"Endpoint
* 4. "AccessKey管理"AccessKey
*
*
* - AccessKey
* - 使RAM
* - 使STS
*/
public class OSSConfig {
// OSS服务端点 - 根据您的Bucket所在区域填写
// 示例华东1杭州https://oss-cn-hangzhou.aliyuncs.com
// 示例华北2北京https://oss-cn-beijing.aliyuncs.com
// 示例华南1深圳https://oss-cn-shenzhen.aliyuncs.com
public static final String OSS_ENDPOINT = "https://oss-cn-wuhan-lr.aliyuncs.com";
// 阿里云AccessKey ID - 在阿里云控制台的"AccessKey管理"中创建
public static final String OSS_ACCESS_KEY_ID = "LTAI5tAiNrYEtYykvN9xNn3w";
// 阿里云AccessKey Secret - 在阿里云控制台的"AccessKey管理"中创建
public static final String OSS_ACCESS_KEY_SECRET = "JnRHqdTMBIoaONvEPNB8RyypZPADaM";
// OSS存储桶名称 - 在阿里云OSS控制台创建Bucket时指定的名称
public static final String OSS_BUCKET_NAME = "mini-notes";
// OSS文件存储路径前缀 - 所有便签数据将存储在此路径下
public static final String OSS_FILE_PREFIX = "notes/";
// OSS连接超时时间毫秒
public static final int OSS_CONNECTION_TIMEOUT = 300000;
// OSS Socket超时时间毫秒
public static final int OSS_SOCKET_TIMEOUT = 300000;
// OSS最大重试次数
public static final int OSS_MAX_RETRY_COUNT = 30;
}

@ -1,608 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.cloud;
import android.content.Context;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* OSSManager - OSS
*
* OSS
* OSS
*
*
* 使HTTPOSS API
*/
public class OSSManager {
private static final String TAG = "OSSManager";
private Context mContext;
private String mAccessKeyId;
private String mAccessKeySecret;
private String mBucketName;
private String mFilePrefix;
/**
*
* @param context
*/
public OSSManager(Context context) {
mContext = context.getApplicationContext();
loadOSSConfig();
}
/**
* OSS
*/
private void loadOSSConfig() {
mAccessKeyId = OSSConfig.OSS_ACCESS_KEY_ID;
mAccessKeySecret = OSSConfig.OSS_ACCESS_KEY_SECRET;
mBucketName = OSSConfig.OSS_BUCKET_NAME;
mFilePrefix = OSSConfig.OSS_FILE_PREFIX;
Log.d(TAG, "OSS config loaded - Bucket: " + mBucketName);
}
/**
*
* @param username
* @return
*/
public String getFilePath(String username) {
return mFilePrefix + "notes_" + username + ".json";
}
/**
* OSS URL
* @param filePath
* @return URL
*/
private String buildOSSUrl(String filePath) {
try {
// 只对路径中的文件名部分进行编码,保留路径分隔符
String[] pathParts = filePath.split("/");
StringBuilder encodedPath = new StringBuilder();
for (int i = 0; i < pathParts.length; i++) {
if (i > 0) {
encodedPath.append("/");
}
encodedPath.append(URLEncoder.encode(pathParts[i], "UTF-8").replace("+", "%20"));
}
String baseUrl = "https://" + mBucketName + ".oss-cn-wuhan-lr.aliyuncs.com";
return baseUrl + "/" + encodedPath.toString();
} catch (Exception e) {
Log.e(TAG, "Failed to build OSS URL", e);
return "";
}
}
/**
* OSS
* @param filePath
* @param content
* @return
*/
public boolean uploadFile(String filePath, String content) {
HttpURLConnection connection = null;
OutputStream outputStream = null;
InputStream errorStream = null;
try {
String urlStr = buildOSSUrl(filePath);
Log.d(TAG, "Building URL for upload: " + filePath);
Log.d(TAG, "Generated URL: " + urlStr);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("PUT");
connection.setDoOutput(true);
connection.setConnectTimeout(OSSConfig.OSS_CONNECTION_TIMEOUT);
connection.setReadTimeout(OSSConfig.OSS_SOCKET_TIMEOUT);
byte[] data = content.getBytes("UTF-8");
String contentMD5 = "";
String contentType = "application/json; charset=utf-8";
String date = getGMTDate();
connection.setRequestProperty("Content-Length", String.valueOf(data.length));
connection.setRequestProperty("Content-Type", contentType);
connection.setRequestProperty("Date", date);
String canonicalizedResource = "/" + mBucketName+ "/" + filePath;
String signature = generateSignature("PUT", contentMD5, contentType, date, canonicalizedResource);
connection.setRequestProperty("Authorization", "OSS " + mAccessKeyId + ":" + signature);
Log.d(TAG, "Upload URL: " + urlStr);
Log.d(TAG, "CanonicalizedResource: " + canonicalizedResource);
Log.d(TAG, "Date: " + date);
Log.d(TAG, "Signature: " + signature);
Log.d(TAG, "Content length: " + data.length);
// 尝试建立连接
Log.d(TAG, "Connecting to OSS...");
connection.connect();
// 写入数据
Log.d(TAG, "Writing data to OSS...");
outputStream = connection.getOutputStream();
outputStream.write(data);
outputStream.flush();
Log.d(TAG, "Data written successfully");
// 获取响应
Log.d(TAG, "Getting response from OSS...");
int responseCode = connection.getResponseCode();
Log.d(TAG, "Upload response code: " + responseCode);
// 获取响应消息
String responseMessage = connection.getResponseMessage();
Log.d(TAG, "Upload response message: " + responseMessage);
if (responseCode == 200 || responseCode == 201) {
Log.d(TAG, "File uploaded successfully: " + filePath);
return true;
} else {
errorStream = connection.getErrorStream();
if (errorStream != null) {
String errorResponse = readStream(errorStream);
Log.e(TAG, "Upload error response: " + errorResponse);
} else {
Log.e(TAG, "No error stream available for response code: " + responseCode);
// 尝试获取响应头信息
Log.e(TAG, "Response headers:");
for (int i = 0; ; i++) {
String headerName = connection.getHeaderFieldKey(i);
if (headerName == null) break;
String headerValue = connection.getHeaderField(i);
Log.e(TAG, headerName + ": " + headerValue);
}
}
Log.e(TAG, "Upload failed with response code: " + responseCode + ", message: " + responseMessage);
return false;
}
} catch (IOException e) {
Log.e(TAG, "IO error during upload", e);
return false;
} catch (Exception e) {
Log.e(TAG, "OSS error during upload", e);
return false;
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (IOException e) {
Log.e(TAG, "Error closing connection", e);
}
}
}
/**
* OSS
* @param filePath
* @return null
*/
public String downloadFile(String filePath) {
HttpURLConnection connection = null;
InputStream inputStream = null;
InputStream errorStream = null;
BufferedReader reader = null;
try {
String urlStr = buildOSSUrl(filePath);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(OSSConfig.OSS_CONNECTION_TIMEOUT);
connection.setReadTimeout(OSSConfig.OSS_SOCKET_TIMEOUT);
String date = getGMTDate();
String contentMD5 = "";
String contentType = "";
connection.setRequestProperty("Date", date);
String canonicalizedResource = "/" + mBucketName+ "/" + filePath;
String signature = generateSignature("GET", contentMD5, contentType, date, canonicalizedResource);
connection.setRequestProperty("Authorization", "OSS " + mAccessKeyId + ":" + signature);
Log.d(TAG, "Download URL: " + urlStr);
Log.d(TAG, "CanonicalizedResource: " + canonicalizedResource);
Log.d(TAG, "Date: " + date);
Log.d(TAG, "Signature: " + signature);
int responseCode = connection.getResponseCode();
Log.d(TAG, "Download response code: " + responseCode);
if (responseCode == 200) {
inputStream = connection.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
Log.d(TAG, "File downloaded successfully: " + filePath);
Log.d(TAG, "Content length: " + content.length());
return content.toString();
} else if (responseCode == 404) {
Log.d(TAG, "File does not exist in OSS: " + filePath);
return null;
} else {
errorStream = connection.getErrorStream();
if (errorStream != null) {
String errorResponse = readStream(errorStream);
Log.e(TAG, "Download error response: " + errorResponse);
}
Log.e(TAG, "Download failed with response code: " + responseCode);
return null;
}
} catch (Exception e) {
Log.e(TAG, "OSS error during download", e);
return null;
} finally {
try {
if (reader != null) {
reader.close();
}
if (inputStream != null) {
inputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (IOException e) {
Log.e(TAG, "Error closing connection", e);
}
}
}
/**
*
* @param filePath
* @return
*/
public boolean doesFileExist(String filePath) {
HttpURLConnection connection = null;
InputStream errorStream = null;
try {
String urlStr = buildOSSUrl(filePath);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(OSSConfig.OSS_CONNECTION_TIMEOUT);
connection.setReadTimeout(OSSConfig.OSS_SOCKET_TIMEOUT);
String date = getGMTDate();
String contentMD5 = "";
String contentType = "";
connection.setRequestProperty("Date", date);
String canonicalizedResource = "/" + mBucketName+ "/" + filePath;
String signature = generateSignature("HEAD", contentMD5, contentType, date, canonicalizedResource);
connection.setRequestProperty("Authorization", "OSS " + mAccessKeyId + ":" + signature);
int responseCode = connection.getResponseCode();
boolean exists = (responseCode == 200);
Log.d(TAG, "File exists check: " + filePath + " = " + exists + " (response code: " + responseCode + ")");
if (!exists && responseCode != 404) {
errorStream = connection.getErrorStream();
if (errorStream != null) {
String errorResponse = readStream(errorStream);
Log.e(TAG, "Existence check error response: " + errorResponse);
}
}
return exists;
} catch (Exception e) {
Log.e(TAG, "OSS error during existence check", e);
return false;
} finally {
try {
if (errorStream != null) {
errorStream.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (IOException e) {
Log.e(TAG, "Error closing connection", e);
}
}
}
/**
* OSS
*/
public void release() {
Log.d(TAG, "OSS client released");
}
/**
* OSS
* @return
*/
public boolean isConfigValid() {
return !mAccessKeyId.equals("your_access_key_id_here") &&
!mAccessKeySecret.equals("your_access_key_secret_here") &&
!mBucketName.equals("your_bucket_name_here");
}
/**
* GMT
* @return GMT
*/
private String getGMTDate() {
SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return dateFormat.format(new Date());
}
/**
* OSS
* @param method HTTP
* @param contentMD5 Content-MD5
* @param contentType Content-Type
* @param date Date
* @param canonicalizedResource
* @return
*/
private String generateSignature(String method, String contentMD5, String contentType, String date, String canonicalizedResource) {
try {
String stringToSign = method + "\n" +
(contentMD5 != null ? contentMD5 : "") + "\n" +
(contentType != null ? contentType : "") + "\n" +
date + "\n" +
canonicalizedResource;
Log.d(TAG, "String to sign: \"" + stringToSign.replace("\n", "\\n") + "\"");
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec secretKeySpec = new SecretKeySpec(mAccessKeySecret.getBytes("UTF-8"), "HmacSHA1");
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(stringToSign.getBytes("UTF-8"));
String signature = new String(android.util.Base64.encode(signatureBytes, android.util.Base64.NO_WRAP));
return signature;
} catch (Exception e) {
Log.e(TAG, "Failed to generate signature", e);
return "";
}
}
/**
*
* @param inputStream
* @return
*/
private String readStream(InputStream inputStream) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} catch (Exception e) {
Log.e(TAG, "Failed to read stream", e);
return "";
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
Log.e(TAG, "Error closing reader", e);
}
}
}
/**
* OSS
* @param filePath
* @param data
* @param contentType
* @return
*/
public boolean uploadBinaryFile(String filePath, byte[] data, String contentType) {
HttpURLConnection connection = null;
OutputStream outputStream = null;
InputStream errorStream = null;
try {
String urlStr = buildOSSUrl(filePath);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("PUT");
connection.setDoOutput(true);
connection.setConnectTimeout(OSSConfig.OSS_CONNECTION_TIMEOUT);
connection.setReadTimeout(OSSConfig.OSS_SOCKET_TIMEOUT);
String date = getGMTDate();
String contentMD5 = "";
connection.setRequestProperty("Content-Length", String.valueOf(data.length));
connection.setRequestProperty("Content-Type", contentType);
connection.setRequestProperty("Date", date);
String canonicalizedResource = "/" + mBucketName + "/" + filePath;
String signature = generateSignature("PUT", contentMD5, contentType, date, canonicalizedResource);
connection.setRequestProperty("Authorization", "OSS " + mAccessKeyId + ":" + signature);
Log.d(TAG, "Upload binary file URL: " + urlStr);
Log.d(TAG, "Content length: " + data.length);
connection.connect();
outputStream = connection.getOutputStream();
outputStream.write(data);
outputStream.flush();
int responseCode = connection.getResponseCode();
Log.d(TAG, "Upload binary file response code: " + responseCode);
if (responseCode == 200 || responseCode == 201) {
Log.d(TAG, "Binary file uploaded successfully: " + filePath);
return true;
} else {
errorStream = connection.getErrorStream();
if (errorStream != null) {
String errorResponse = readStream(errorStream);
Log.e(TAG, "Upload binary file error response: " + errorResponse);
}
Log.e(TAG, "Upload binary file failed with response code: " + responseCode);
return false;
}
} catch (Exception e) {
Log.e(TAG, "Failed to upload binary file", e);
return false;
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (IOException e) {
Log.e(TAG, "Error closing connection", e);
}
}
}
/**
* OSS
* @param filePath
* @return null
*/
public byte[] downloadBinaryFile(String filePath) {
HttpURLConnection connection = null;
InputStream inputStream = null;
InputStream errorStream = null;
try {
String urlStr = buildOSSUrl(filePath);
URL url = new URL(urlStr);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(OSSConfig.OSS_CONNECTION_TIMEOUT);
connection.setReadTimeout(OSSConfig.OSS_SOCKET_TIMEOUT);
String date = getGMTDate();
String contentMD5 = "";
String contentType = "";
connection.setRequestProperty("Date", date);
String canonicalizedResource = "/" + mBucketName + "/" + filePath;
String signature = generateSignature("GET", contentMD5, contentType, date, canonicalizedResource);
connection.setRequestProperty("Authorization", "OSS " + mAccessKeyId + ":" + signature);
Log.d(TAG, "Download binary file URL: " + urlStr);
int responseCode = connection.getResponseCode();
Log.d(TAG, "Download binary file response code: " + responseCode);
if (responseCode == 200) {
inputStream = connection.getInputStream();
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = inputStream.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
byte[] data = baos.toByteArray();
Log.d(TAG, "Binary file downloaded successfully: " + filePath + ", size: " + data.length);
return data;
} else if (responseCode == 404) {
Log.d(TAG, "Binary file does not exist in OSS: " + filePath);
return null;
} else {
errorStream = connection.getErrorStream();
if (errorStream != null) {
String errorResponse = readStream(errorStream);
Log.e(TAG, "Download binary file error response: " + errorResponse);
}
Log.e(TAG, "Download binary file failed with response code: " + responseCode);
return null;
}
} catch (Exception e) {
Log.e(TAG, "Failed to download binary file", e);
return null;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (errorStream != null) {
errorStream.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (IOException e) {
Log.e(TAG, "Error closing connection", e);
}
}
}
}

@ -1,132 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.cloud;
import android.content.Context;
import android.app.AlertDialog;
import android.content.DialogInterface;
/**
* SyncDialogManager -
*
*
*
*
* UI
*/
public class SyncDialogManager {
private static final String TAG = "SyncDialogManager";
/**
*
*/
public interface SyncOptionListener {
/**
*
*/
void onUploadSelected();
/**
*
*/
void onDownloadSelected();
/**
*
*/
void onCanceled();
}
/**
*
*/
public interface ConfirmListener {
/**
*
*/
void onConfirm();
/**
*
*/
void onCancel();
}
/**
*
* @param context
* @param listener
*/
public static void showSyncOptionsDialog(Context context, final SyncOptionListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("同步操作选择")
.setMessage("请选择要执行的同步操作")
.setNegativeButton("下载", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener != null) {
listener.onDownloadSelected();
}
}
})
.setPositiveButton("上传", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener != null) {
listener.onUploadSelected();
}
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (listener != null) {
listener.onCanceled();
}
}
})
.show();
}
/**
*
* @param context
* @param listener
*/
public static void showUploadConfirmDialog(Context context, final ConfirmListener listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("上传确认")
.setMessage("上传会覆盖云端内容,确定要继续吗?")
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener != null) {
listener.onCancel();
}
}
})
.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (listener != null) {
listener.onConfirm();
}
}
})
.show();
}
}

@ -1,494 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.cloud;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.account.AccountManager;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.NoteSyncUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
/**
* SyncManager -
*
*
*
*
*
*/
public class SyncManager {
private static final String TAG = "SyncManager";
private static SyncManager sInstance;
private Context mContext;
private OSSManager mOssManager;
private boolean mIsSyncing;
/**
* SyncManager
* @param context
* @return SyncManager
*/
public static synchronized SyncManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new SyncManager(context.getApplicationContext());
}
return sInstance;
}
/**
*
* @param context
*/
private SyncManager(Context context) {
mContext = context;
mOssManager = new OSSManager(context);
mIsSyncing = false;
}
/**
*
* @return
*/
public boolean isSyncing() {
return mIsSyncing;
}
/**
*
* @param syncing
*/
public void setSyncing(boolean syncing) {
mIsSyncing = syncing;
}
/**
*
* @param callback
*/
public void sync(SyncCallback callback) {
if (mIsSyncing) {
Log.d(TAG, "Sync already in progress");
if (callback != null) {
callback.onSyncFailed("同步已在进行中");
}
return;
}
// 检查OSS配置是否有效
if (!mOssManager.isConfigValid()) {
Log.w(TAG, "OSS configuration is invalid");
if (callback != null) {
callback.onSyncFailed("OSS配置无效请检查配置文件");
}
return;
}
// 检查用户登录状态
if (!AccountManager.isUserLoggedIn(mContext)) {
Log.w(TAG, "User not logged in");
if (callback != null) {
callback.onSyncFailed("请先登录后再同步");
}
return;
}
String username = AccountManager.getCurrentUser(mContext);
if (username.isEmpty()) {
Log.w(TAG, "Empty username");
if (callback != null) {
callback.onSyncFailed("用户名为空");
}
return;
}
// 执行同步任务
new SyncTask(mContext, this, username, callback).execute();
}
/**
*
* @param username
* @return
*/
public boolean uploadNotes(String username) {
try {
// 获取本地所有笔记数据
String jsonContent = NoteSyncUtils.localNotesToJson(mContext);
if (jsonContent == null) {
Log.e(TAG, "Failed to convert local notes to JSON");
return false;
}
// 解析JSON获取附件列表
JSONObject root = new JSONObject(jsonContent);
JSONArray notesArray = root.optJSONArray("notes");
if (notesArray != null) {
for (int i = 0; i < notesArray.length(); i++) {
JSONObject noteObj = notesArray.optJSONObject(i);
if (noteObj != null) {
JSONArray attachmentsArray = noteObj.optJSONArray("attachments");
if (attachmentsArray != null) {
for (int j = 0; j < attachmentsArray.length(); j++) {
JSONObject attachmentObj = attachmentsArray.optJSONObject(j);
if (attachmentObj != null) {
String filePath = attachmentObj.optString("file_path", "");
if (!filePath.isEmpty()) {
// 上传附件文件到OSS
uploadAttachmentFile(username, filePath);
}
}
}
}
}
}
}
// 上传JSON数据到OSS
String filePath = mOssManager.getFilePath(username);
boolean success = mOssManager.uploadFile(filePath, jsonContent);
Log.d(TAG, "Upload notes result: " + success);
return success;
} catch (Exception e) {
Log.e(TAG, "Failed to upload notes", e);
return false;
}
}
/**
* OSS
* @param username
* @param localFilePath
* @return
*/
private boolean uploadAttachmentFile(String username, String localFilePath) {
try {
File file = new File(localFilePath);
if (!file.exists()) {
Log.w(TAG, "Attachment file does not exist: " + localFilePath);
return false;
}
// 读取文件内容
java.io.FileInputStream fis = new java.io.FileInputStream(file);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
fis.close();
byte[] data = baos.toByteArray();
// 构建OSS文件路径
String fileName = file.getName();
String ossFilePath = OSSConfig.OSS_FILE_PREFIX + "attachments/" + username + "/" + fileName;
// 上传到OSS
String contentType = "image/jpeg"; // 默认图片类型
if (fileName.toLowerCase().endsWith(".png")) {
contentType = "image/png";
} else if (fileName.toLowerCase().endsWith(".gif")) {
contentType = "image/gif";
}
boolean success = mOssManager.uploadBinaryFile(ossFilePath, data, contentType);
if (success) {
Log.d(TAG, "Attachment uploaded successfully: " + ossFilePath);
} else {
Log.e(TAG, "Failed to upload attachment: " + ossFilePath);
}
return success;
} catch (Exception e) {
Log.e(TAG, "Failed to upload attachment file: " + localFilePath, e);
return false;
}
}
/**
*
* @param username
* @return
*/
public boolean downloadNotes(String username) {
try {
// 从OSS下载
String filePath = mOssManager.getFilePath(username);
String jsonContent = mOssManager.downloadFile(filePath);
if (jsonContent == null) {
Log.e(TAG, "Failed to download notes from OSS");
return false;
}
// 解析JSON获取附件列表
JSONObject root = new JSONObject(jsonContent);
JSONArray notesArray = root.optJSONArray("notes");
if (notesArray != null) {
for (int i = 0; i < notesArray.length(); i++) {
JSONObject noteObj = notesArray.optJSONObject(i);
if (noteObj != null) {
JSONArray attachmentsArray = noteObj.optJSONArray("attachments");
if (attachmentsArray != null) {
for (int j = 0; j < attachmentsArray.length(); j++) {
JSONObject attachmentObj = attachmentsArray.optJSONObject(j);
if (attachmentObj != null) {
String filePath2 = attachmentObj.optString("file_path", "");
if (!filePath2.isEmpty()) {
// 下载附件文件从OSS
downloadAttachmentFile(username, filePath2);
}
}
}
}
}
}
}
// 解析并合并数据
boolean success = NoteSyncUtils.jsonToLocalNotes(mContext, jsonContent);
Log.d(TAG, "Download notes result: " + success);
return success;
} catch (Exception e) {
Log.e(TAG, "Failed to download notes", e);
return false;
}
}
/**
* OSS
* @param username
* @param originalFilePath
* @return
*/
private boolean downloadAttachmentFile(String username, String originalFilePath) {
try {
// 获取文件名
File originalFile = new File(originalFilePath);
String fileName = originalFile.getName();
// 构建OSS文件路径
String ossFilePath = OSSConfig.OSS_FILE_PREFIX + "attachments/" + username + "/" + fileName;
// 从OSS下载文件
byte[] data = mOssManager.downloadBinaryFile(ossFilePath);
if (data == null) {
Log.w(TAG, "Attachment not found in OSS: " + ossFilePath);
return false;
}
// 获取附件存储目录
AttachmentManager attachmentManager = new AttachmentManager(mContext);
File storageDir = attachmentManager.getAttachmentStorageDir();
if (!storageDir.exists()) {
storageDir.mkdirs();
}
// 保存文件到本地
File destFile = new File(storageDir, fileName);
FileOutputStream fos = new FileOutputStream(destFile);
fos.write(data);
fos.close();
Log.d(TAG, "Attachment downloaded successfully: " + destFile.getAbsolutePath());
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to download attachment file: " + originalFilePath, e);
return false;
}
}
/**
*
*/
public interface SyncCallback {
/**
*
*/
void onSyncStart();
/**
*
*/
void onSyncSuccess();
/**
*
* @param errorMessage
*/
void onSyncFailed(String errorMessage);
}
/**
*
* @param callback
*/
public void triggerUpload(final SyncCallback callback) {
if (mIsSyncing) {
Log.d(TAG, "Upload already in progress");
if (callback != null) {
callback.onSyncFailed("上传已在进行中");
}
return;
}
// 检查OSS配置是否有效
if (!mOssManager.isConfigValid()) {
Log.w(TAG, "OSS configuration is invalid");
if (callback != null) {
callback.onSyncFailed("OSS配置无效请检查配置文件");
}
return;
}
// 检查用户登录状态
if (!AccountManager.isUserLoggedIn(mContext)) {
Log.w(TAG, "User not logged in");
if (callback != null) {
callback.onSyncFailed("请先登录后再同步");
}
return;
}
final String username = AccountManager.getCurrentUser(mContext);
if (username.isEmpty()) {
Log.w(TAG, "Empty username");
if (callback != null) {
callback.onSyncFailed("用户名为空");
}
return;
}
// 设置同步状态
setSyncing(true);
// 通知同步开始
if (callback != null) {
callback.onSyncStart();
}
// 在后台执行上传操作
new Thread(new Runnable() {
@Override
public void run() {
try {
boolean success = uploadNotes(username);
setSyncing(false);
if (callback != null) {
if (success) {
callback.onSyncSuccess();
} else {
callback.onSyncFailed("上传失败,请重试");
}
}
} catch (Exception e) {
Log.e(TAG, "Upload failed", e);
setSyncing(false);
if (callback != null) {
callback.onSyncFailed("上传失败:" + e.getMessage());
}
}
}
}).start();
}
/**
*
* @param callback
*/
public void triggerDownload(final SyncCallback callback) {
if (mIsSyncing) {
Log.d(TAG, "Download already in progress");
if (callback != null) {
callback.onSyncFailed("下载已在进行中");
}
return;
}
// 检查OSS配置是否有效
if (!mOssManager.isConfigValid()) {
Log.w(TAG, "OSS configuration is invalid");
if (callback != null) {
callback.onSyncFailed("OSS配置无效请检查配置文件");
}
return;
}
// 检查用户登录状态
if (!AccountManager.isUserLoggedIn(mContext)) {
Log.w(TAG, "User not logged in");
if (callback != null) {
callback.onSyncFailed("请先登录后再同步");
}
return;
}
final String username = AccountManager.getCurrentUser(mContext);
if (username.isEmpty()) {
Log.w(TAG, "Empty username");
if (callback != null) {
callback.onSyncFailed("用户名为空");
}
return;
}
// 设置同步状态
setSyncing(true);
// 通知同步开始
if (callback != null) {
callback.onSyncStart();
}
// 在后台执行下载操作
new Thread(new Runnable() {
@Override
public void run() {
try {
boolean success = downloadNotes(username);
setSyncing(false);
if (callback != null) {
if (success) {
callback.onSyncSuccess();
} else {
callback.onSyncFailed("下载失败,请重试");
}
}
} catch (Exception e) {
Log.e(TAG, "Download failed", e);
setSyncing(false);
if (callback != null) {
callback.onSyncFailed("下载失败:" + e.getMessage());
}
}
}
}).start();
}
}

@ -1,152 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.cloud;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
/**
* SyncTask -
*
* AsyncTask
*
*
*/
public class SyncTask extends AsyncTask<Void, Integer, SyncTask.SyncResult> {
private static final String TAG = "SyncTask";
private Context mContext;
private SyncManager mSyncManager;
private String mUsername;
private SyncManager.SyncCallback mCallback;
/**
*
*/
public static class SyncResult {
boolean success;
String errorMessage;
SyncResult(boolean success, String errorMessage) {
this.success = success;
this.errorMessage = errorMessage;
}
}
/**
*
* @param context
* @param syncManager
* @param username
* @param callback
*/
public SyncTask(Context context, SyncManager syncManager, String username, SyncManager.SyncCallback callback) {
mContext = context;
mSyncManager = syncManager;
mUsername = username;
mCallback = callback;
}
/**
*
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
// 设置同步状态为true
mSyncManager.setSyncing(true);
// 通知同步开始
if (mCallback != null) {
mCallback.onSyncStart();
}
Log.d(TAG, "Sync task started");
}
/**
*
* @param params
* @return
*/
@Override
protected SyncResult doInBackground(Void... params) {
try {
// 1. 先从云端下载数据
Log.d(TAG, "Downloading notes from cloud");
publishProgress(25); // 25% 进度
boolean downloadSuccess = mSyncManager.downloadNotes(mUsername);
if (!downloadSuccess) {
Log.w(TAG, "Download failed, but continue to upload");
}
// 2. 然后上传本地数据到云端
Log.d(TAG, "Uploading notes to cloud");
publishProgress(75); // 75% 进度
boolean uploadSuccess = mSyncManager.uploadNotes(mUsername);
if (!uploadSuccess) {
Log.e(TAG, "Upload failed");
return new SyncResult(false, "上传到云端失败");
}
publishProgress(100); // 100% 进度
Log.d(TAG, "Sync task completed successfully");
return new SyncResult(true, null);
} catch (Exception e) {
Log.e(TAG, "Sync task failed", e);
return new SyncResult(false, "OSS操作失败请稍后重试");
}
}
/**
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
// 可以在这里更新UI进度例如显示进度条
Log.d(TAG, "Sync progress: " + values[0] + "%");
}
/**
*
* @param result
*/
@Override
protected void onPostExecute(SyncResult result) {
super.onPostExecute(result);
// 设置同步状态为false
mSyncManager.setSyncing(false);
// 通知同步结果
if (mCallback != null) {
if (result.success) {
mCallback.onSyncSuccess();
} else {
mCallback.onSyncFailed(result.errorMessage);
}
}
Log.d(TAG, "Sync task finished with result: " + result.success);
}
}

@ -1,344 +0,0 @@
package net.micode.notes.data;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class AttachmentManager {
private static final String TAG = "AttachmentManager";
private final Context mContext;
private final ContentResolver mContentResolver;
/**
*
*/
private static final String ATTACHMENT_DIR = "attachments";
/**
*
* @param context
*/
public AttachmentManager(Context context) {
mContext = context.getApplicationContext();
mContentResolver = mContext.getContentResolver();
}
/**
*
* @param noteId ID
* @param type Notes.ATTACHMENT_TYPE_GALLERY Notes.ATTACHMENT_TYPE_CAMERA
* @param sourceFile
* @return ID-1
*/
public long addAttachment(long noteId, int type, File sourceFile) {
if (sourceFile == null || !sourceFile.exists()) {
Log.e(TAG, "Source file does not exist");
return -1;
}
// 复制文件到应用私有存储
File destFile = copyToPrivateStorage(sourceFile, noteId);
if (destFile == null) {
Log.e(TAG, "Failed to copy file to private storage");
return -1;
}
// 插入数据库记录
ContentValues values = new ContentValues();
values.put(Notes.AttachmentColumns.NOTE_ID, noteId);
values.put(Notes.AttachmentColumns.TYPE, type);
values.put(Notes.AttachmentColumns.FILE_PATH, destFile.getAbsolutePath());
values.put(Notes.AttachmentColumns.CREATED_TIME, System.currentTimeMillis());
try {
Uri uri = mContentResolver.insert(Notes.CONTENT_ATTACHMENT_URI, values);
if (uri != null) {
String idStr = uri.getLastPathSegment();
if (!TextUtils.isEmpty(idStr)) {
long attachmentId = Long.parseLong(idStr);
// 更新笔记的附件状态
updateNoteAttachmentStatus(noteId, true);
return attachmentId;
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to insert attachment", e);
// 插入失败时删除已复制的文件
destFile.delete();
}
return -1;
}
/**
*
* @param attachmentId ID
* @return
*/
public boolean deleteAttachment(long attachmentId) {
// 先获取笔记ID
long noteId = getNoteIdByAttachmentId(attachmentId);
if (noteId <= 0) {
Log.e(TAG, "Failed to get note id for attachment " + attachmentId);
return false;
}
// 获取文件路径并删除文件
String filePath = getAttachmentFilePath(attachmentId);
if (filePath != null) {
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
}
// 删除数据库记录
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
int count = mContentResolver.delete(uri, null, null);
boolean success = count > 0;
if (success) {
// 检查笔记是否还有其他附件
List<Attachment> remainingAttachments = getAttachmentsByNoteId(noteId);
if (remainingAttachments.isEmpty()) {
// 没有附件了,更新笔记状态
updateNoteAttachmentStatus(noteId, false);
}
}
return success;
}
/**
*
* @param noteId ID
* @return
*/
public int deleteAttachmentsByNoteId(long noteId) {
List<Attachment> attachments = getAttachmentsByNoteId(noteId);
int deletedCount = 0;
for (Attachment attachment : attachments) {
if (deleteAttachment(attachment.id)) {
deletedCount++;
}
}
return deletedCount;
}
/**
*
* @param attachmentId ID
* @return null
*/
public String getAttachmentFilePath(long attachmentId) {
String[] projection = { Notes.AttachmentColumns.FILE_PATH };
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
Cursor cursor = null;
try {
cursor = mContentResolver.query(uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(0);
}
} catch (Exception e) {
Log.e(TAG, "Failed to get attachment file path", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
/**
*
* @param noteId ID
* @return
*/
public List<Attachment> getAttachmentsByNoteId(long noteId) {
List<Attachment> attachments = new ArrayList<>();
String[] projection = {
Notes.AttachmentColumns.ID,
Notes.AttachmentColumns.NOTE_ID,
Notes.AttachmentColumns.TYPE,
Notes.AttachmentColumns.FILE_PATH,
Notes.AttachmentColumns.CREATED_TIME
};
String selection = Notes.AttachmentColumns.NOTE_ID + "=?";
String[] selectionArgs = { String.valueOf(noteId) };
String sortOrder = Notes.AttachmentColumns.CREATED_TIME + " ASC";
Cursor cursor = null;
try {
cursor = mContentResolver.query(Notes.CONTENT_ATTACHMENT_URI,
projection, selection, selectionArgs, sortOrder);
if (cursor != null) {
while (cursor.moveToNext()) {
Attachment attachment = new Attachment();
attachment.id = cursor.getLong(0);
attachment.noteId = cursor.getLong(1);
attachment.type = cursor.getInt(2);
attachment.filePath = cursor.getString(3);
attachment.createdTime = cursor.getLong(4);
attachments.add(attachment);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to get attachments", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return attachments;
}
/**
*
* @param sourceFile
* @param noteId ID
* @return null
*/
private File copyToPrivateStorage(File sourceFile, long noteId) {
File destDir = getAttachmentStorageDir();
if (destDir == null || !destDir.exists() && !destDir.mkdirs()) {
Log.e(TAG, "Failed to create attachment directory");
return null;
}
// 生成唯一文件名note_{noteId}_{timestamp}_{random}.jpg
String timestamp = String.valueOf(System.currentTimeMillis());
String random = String.valueOf((int)(Math.random() * 1000));
String extension = getFileExtension(sourceFile.getName());
String fileName = "note_" + noteId + "_" + timestamp + "_" + random + extension;
File destFile = new File(destDir, fileName);
try {
copyFile(sourceFile, destFile);
return destFile;
} catch (IOException e) {
Log.e(TAG, "Failed to copy file", e);
return null;
}
}
/**
*
* @return
*/
public File getAttachmentStorageDir() {
File picturesDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (picturesDir == null) {
picturesDir = new File(mContext.getFilesDir(), "Pictures");
}
return new File(picturesDir, ATTACHMENT_DIR);
}
/**
*
* @param fileName
* @return
*/
private String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex);
}
return ".jpg";
}
/**
*
* @param source
* @param dest
* @throws IOException IO
*/
private void copyFile(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
destChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
}
}
/**
* IDID
* @param attachmentId ID
* @return ID-1
*/
private long getNoteIdByAttachmentId(long attachmentId) {
String[] projection = { Notes.AttachmentColumns.NOTE_ID };
Uri uri = Uri.withAppendedPath(Notes.CONTENT_ATTACHMENT_URI, String.valueOf(attachmentId));
Cursor cursor = null;
try {
cursor = mContentResolver.query(uri, projection, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(0);
}
} catch (Exception e) {
Log.e(TAG, "Failed to get note id by attachment id", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
return -1;
}
/**
*
* @param noteId ID
* @param hasAttachment
*/
private void updateNoteAttachmentStatus(long noteId, boolean hasAttachment) {
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.HAS_ATTACHMENT, hasAttachment ? 1 : 0);
values.put(Notes.NoteColumns.LOCAL_MODIFIED, 1);
values.put(Notes.NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
try {
mContentResolver.update(uri, values, null, null);
} catch (Exception e) {
Log.e(TAG, "Failed to update note attachment status", e);
}
}
/**
*
*/
public static class Attachment {
public long id;
public long noteId;
public int type;
public String filePath;
public long createdTime;
@Override
public String toString() {
return "Attachment{" +
"id=" + id +
", noteId=" + noteId +
", type=" + type +
", filePath='" + filePath + '\'' +
", createdTime=" + createdTime +
'}';
}
}
}

@ -25,17 +25,10 @@ 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 "
@ -43,50 +36,36 @@ public class Contact {
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
/**
*
* @param context
* @param phoneNumber
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
if( sContactCache == null) {
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, // 查询的URI
new String [] { Phone.DISPLAY_NAME }, // 返回的列名:联系人显示名称
selection, // 查询条件
new String[] { phoneNumber }, // 查询参数
null); // 排序方式
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;
}

@ -17,188 +17,50 @@
package net.micode.notes.data;
import android.net.Uri;
/**
* 便URI
* 广
*/
public class Notes {
/**
* ContentProviderURI
*/
public static final String AUTHORITY = "micode_notes";
/**
*
*/
public static final String TAG = "Notes";
/**
*
*/
public static final int TYPE_NOTE = 0;
/**
*
*/
public static final int TYPE_FOLDER = 1;
/**
*
*/
public static final int TYPE_SYSTEM = 2;
/**
* ID
*/
/**
* ID
* 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;
/**
* ID
*/
public static final int ID_TEMPARAY_FOLDER = -1;
/**
* ID
*/
public static final int ID_CALL_RECORD_FOLDER = -2;
/**
* ID
*/
public static final int ID_TRASH_FOLDER = -3;
public static final int ID_TRASH_FOLER = -3;
/**
* Intent
*/
/**
*
*/
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
/**
* ID
*/
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
/**
* ID
*/
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
/**
*
*/
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
/**
* ID
*/
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;
/**
* 2x
*/
public static final int TYPE_WIDGET_2X = 0;
/**
* 4x
*/
public static final int TYPE_WIDGET_4X = 1;
/**
* MIME
*/
public static class DataConstants {
/**
* MIME
*/
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
/**
* MIME
*/
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
*
*/
public static final int ATTACHMENT_TYPE_GALLERY = 0;
public static final int ATTACHMENT_TYPE_CAMERA = 1;
/**
*
*/
public interface AttachmentColumns {
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* The note's id this attachment belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
* Attachment type (0=gallery, 1=camera)
* <P> Type: INTEGER </P>
*/
public static final String TYPE = "type";
/**
* File path in private storage
* <P> Type: TEXT </P>
*/
public static final String FILE_PATH = "file_path";
/**
* Created time
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_TIME = "created_time";
}
/**
* Uri
* Uri to query all notes and folders
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri
* Uri to query data
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
* Uri
*/
public static final Uri CONTENT_ATTACHMENT_URI = Uri.parse("content://" + AUTHORITY + "/attachment");
/**
*
*/
public interface NoteColumns {
/**
* The unique ID for a row
@ -303,17 +165,8 @@ public class Notes {
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
/**
* Whether the note is locked with password
* <P> Type : INTEGER </P>
*/
public static final String IS_LOCKED = "is_locked";
}
/**
*
*/
public interface DataColumns {
/**
* The unique ID for a row
@ -388,9 +241,6 @@ public class Notes {
public static final String DATA5 = "data5";
}
/**
* DataColumns
*/
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
@ -406,10 +256,7 @@ public class Notes {
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
/**
* DataColumns
*/
public static final class CallNote implements DataColumns {
/**
* Call date for this record

@ -18,7 +18,6 @@ package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
@ -28,75 +27,21 @@ import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* (Data Layer - Database Helper)
* <p>
*
* 1. SQLite (onCreate, onUpgrade)
* 2. 便
* 3. SQL (SNIPPET)
* 4.
* <p>
* 线
* {@link NotesProvider}
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
*
*/
private static final String DB_NAME = "note.db";
/**
* {@link #onUpgrade}
*/
private static final int DB_VERSION = 8;
private static final int DB_VERSION = 4;
/**
*
* <p>
*
* 1. {@link #NOTE}便
* 2. {@link #DATA}便 MIME
* 3. {@link #ATTACHMENT}便
*/
public interface TABLE {
/**
*
*/
public static final String NOTE = "note";
/**
*
*/
public static final String DATA = "data";
/**
*
*/
public static final String ATTACHMENT = "note_attachment";
}
/**
*
*/
private static final String TAG = "NotesDatabaseHelper";
/**
* {@link #getInstance(Context)}
*/
private static NotesDatabaseHelper mInstance;
/**
* 便 (note) SQL
* <p>
*
* - {@link NoteColumns#PARENT_ID}: ID
* - {@link NoteColumns#SNIPPET}: data
* - {@link NoteColumns#TYPE}:
* - {@link NoteColumns#BG_COLOR_ID}: ID2025 AI AI
* - {@link NoteColumns#GTASK_ID}: Google ID
* - {@link NoteColumns#VERSION}:
*/
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
@ -115,20 +60,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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," +
NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
/**
* (data) SQL
* <p>
* EAV (Entity-Attribute-Value)
*
* - {@link DataColumns#MIME_TYPE}: CONTENT
* - {@link DataColumns#NOTE_ID}: note ID
* - {@link DataColumns#CONTENT}:
* - DATA1~DATA5: MIME_TYPE
*/
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
@ -144,37 +78,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
/**
* (note_attachment) SQL
* <p>
* 便
*
* - _id: Android ContentProvider
* - note_id: ID
* - type: 0=1=
* - file_path:
* - created_time:
*/
private static final String CREATE_ATTACHMENT_TABLE_SQL =
"CREATE TABLE " + TABLE.ATTACHMENT + "(" +
"_id INTEGER PRIMARY KEY," +
"note_id INTEGER NOT NULL," +
"type INTEGER NOT NULL DEFAULT 0," +
"file_path TEXT NOT NULL," +
"created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")";
/**
* data NOTE_ID SQL
* <p>
* 便 ID
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/**
* 便便
* 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 "+
@ -186,9 +95,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* 便便
* <p>
* 0
* 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 " +
@ -201,7 +108,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" 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 " +
@ -213,9 +120,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* 便
* <p>
* 0
* 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 " +
@ -228,10 +133,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* data {@link DataConstants#NOTE}
* note 便 {@link NoteColumns#SNIPPET}
* <p>
*
* 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 " +
@ -244,7 +146,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* data {@link DataConstants#NOTE} 便
* 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 " +
@ -257,7 +159,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* data {@link DataConstants#NOTE} 便
* 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 " +
@ -270,10 +172,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* note 便/
* data ({@link DataColumns#NOTE_ID})
* <p>
* 使 FOREIGN KEY
* 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 " +
@ -284,10 +183,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* TYPE
* 便 PARENT_ID
* <p>
* {@link #NOTE_DELETE_DATA_ON_DELETE_TRIGGER}
* 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 " +
@ -298,60 +194,30 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" END";
/**
* PARENT_ID {@link Notes#ID_TRASH_FOLER}
* 便 PARENT_ID ID
* <p>
*
* 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_FOLDER +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLDER +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* {@link #getInstance(Context)}
*
* @param context
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* 便 (note)
* <p>
*
* 1. SQL
* 2.
* 3.
*
* @param db
*/
public void createNoteTable(SQLiteDatabase db) {
// 执行创建表SQL
db.execSQL(CREATE_NOTE_TABLE_SQL);
// 重新创建笔记表相关触发器
reCreateNoteTableTriggers(db);
// 创建系统文件夹
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
/**
* note SQL
* <p>
*
*
*
* @param db
*/
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");
@ -360,7 +226,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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);
@ -370,17 +235,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/**
*
* <p>
*
* 1. ({@link Notes#ID_CALL_RECORD_FOLDER})
* 2. ({@link Notes#ID_ROOT_FOLDER})
* 3. ({@link Notes#ID_TEMPARAY_FOLDER})便
* 4. ({@link Notes#ID_TRASH_FOLER})便
*
* @param db
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
@ -411,47 +265,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* create trash folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLDER);
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
* (data)
*
* @param db
*/
public void createDataTable(SQLiteDatabase db) {
// 执行创建表SQL
db.execSQL(CREATE_DATA_TABLE_SQL);
// 重新创建数据表相关触发器
reCreateDataTableTriggers(db);
// 创建note_id索引
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
/**
* (note_attachment)
*
* @param db
*/
public void createAttachmentTable(SQLiteDatabase db) {
// 执行创建表SQL
db.execSQL(CREATE_ATTACHMENT_TABLE_SQL);
// 创建note_id索引以提高查询性能
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON " +
TABLE.ATTACHMENT + "(note_id);");
Log.d(TAG, "attachment table has been created");
}
/**
* data SQL
* <p>
* note SNIPPET data CONTENT
*
* @param db
*/
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");
@ -462,120 +287,52 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
/**
*
* <p>
* 线 synchronized 线
*
* @param context
* @return NotesDatabaseHelper
*/
public static synchronized NotesDatabaseHelper getInstance(Context context) {
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
/**
*
* <p>
* note data
*
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {
// 创建笔记表
createNoteTable(db);
// 创建数据表
createDataTable(db);
// 创建附件表
createAttachmentTable(db);
}
/**
*
* <p>
*
* v1v4
*
* @param db
* @param oldVersion
* @param newVersion {@link #DB_VERSION}
* @throws IllegalStateException
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
// 1. 从版本1升级
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
// 2. 从版本2升级如果未跳过
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true;
oldVersion++;
}
// 3. 从版本3升级
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 4. 从版本4升级到版本5支持图片插入功能
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
// 5. 从版本5升级到版本6添加附件表
if (oldVersion == 5) {
upgradeToV6(db);
oldVersion++;
}
// 6. 从版本6升级到版本7修复附件表主键列名
if (oldVersion == 6) {
upgradeToV7(db);
oldVersion++;
}
// 8. 从版本7升级到版本8添加便签加锁功能
if (oldVersion == 7) {
upgradeToV8(db);
oldVersion++;
}
// 9. 如果表结构在v3升级中发生较大变更需要重建触发器以确保兼容性
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 8. 最终版本校验:确保所有升级步骤已执行完毕
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
/**
* 2
* <p>
* {@link #onCreate}
* 使
* <b></b>
*
* @param db
*/
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@ -583,17 +340,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
/**
* 3
* <p>
* (ALTER TABLE)
*
* 1.
* 2. Google ID ({@link NoteColumns#GTASK_ID})
* 3.
*
* @param db
*/
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
@ -604,167 +350,13 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLDER);
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
* 4
* <p>
*
* ({@link NoteColumns#VERSION})
* <p>
*
*
* @param db
*/
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
/**
* 5
* <p>
*
*
* <p>
*
* - data 使 DATA3DATA4DATA5
* - MIME_TYPE
* -
*
* @param db
*/
private void upgradeToV5(SQLiteDatabase db) {
// 为图片数据添加查询索引,提高图片查询性能
db.execSQL("CREATE INDEX IF NOT EXISTS image_data_index ON " + TABLE.DATA + "(" + DataColumns.MIME_TYPE + ") WHERE "
+ DataColumns.MIME_TYPE + " LIKE 'image/%'");
Log.d(TAG, "Database upgraded to version 5 (image support with index)");
}
/**
* 6
* <p>
*
* 便
*
* @param db
*/
private void upgradeToV6(SQLiteDatabase db) {
// 创建附件表
createAttachmentTable(db);
Log.d(TAG, "Database upgraded to version 6 (attachment table added)");
}
/**
* 7
* <p>
*
* "id" "_id"Android ContentProvider
*
* @param db
*/
private void upgradeToV7(SQLiteDatabase db) {
// 检查附件表是否存在
Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='note_attachment'", null);
boolean tableExists = cursor != null && cursor.getCount() > 0;
if (cursor != null) {
cursor.close();
}
if (tableExists) {
// 检查当前表结构中的主键列名
cursor = db.rawQuery("PRAGMA table_info(note_attachment)", null);
boolean hasIdColumn = false;
boolean hasUnderscoreIdColumn = false;
if (cursor != null) {
while (cursor.moveToNext()) {
String columnName = cursor.getString(1); // 列名在索引1
if ("id".equals(columnName)) {
hasIdColumn = true;
} else if ("_id".equals(columnName)) {
hasUnderscoreIdColumn = true;
}
}
cursor.close();
}
if (hasIdColumn && !hasUnderscoreIdColumn) {
// 需要将id列重命名为_id
Log.d(TAG, "Starting migration: renaming 'id' column to '_id' in note_attachment table");
// 使用事务确保迁移的原子性
db.beginTransaction();
try {
// 1. 创建临时表
db.execSQL("CREATE TABLE note_attachment_temp (" +
"_id INTEGER PRIMARY KEY," +
"note_id INTEGER NOT NULL," +
"type INTEGER NOT NULL DEFAULT 0," +
"file_path TEXT NOT NULL," +
"created_time INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")");
// 2. 复制数据
db.execSQL("INSERT INTO note_attachment_temp (_id, note_id, type, file_path, created_time) " +
"SELECT id, note_id, type, file_path, created_time FROM note_attachment");
// 3. 删除原表
db.execSQL("DROP TABLE note_attachment");
// 4. 重命名临时表
db.execSQL("ALTER TABLE note_attachment_temp RENAME TO note_attachment");
// 5. 重新创建索引
db.execSQL("CREATE INDEX IF NOT EXISTS attachment_note_id_index ON note_attachment(note_id)");
db.setTransactionSuccessful();
Log.d(TAG, "Successfully migrated note_attachment table: renamed 'id' to '_id'");
} catch (Exception e) {
Log.e(TAG, "Failed to migrate note_attachment table", e);
throw e;
} finally {
db.endTransaction();
}
} else if (hasUnderscoreIdColumn) {
Log.d(TAG, "Table already has correct '_id' column, no migration needed");
} else {
Log.w(TAG, "Unexpected table structure for note_attachment, recreating table");
// 如果表结构异常,重新创建表
db.execSQL("DROP TABLE IF EXISTS note_attachment");
createAttachmentTable(db);
}
} else {
// 表不存在,直接创建新表
createAttachmentTable(db);
Log.d(TAG, "Created note_attachment table with correct column names");
}
Log.d(TAG, "Database upgraded to version 7 (attachment table column name fixed)");
}
/**
* 8
* <p>
*
* 便 is_locked
* <p>
*
* - is_locked: INTEGER01
* - 0
*
* @param db
*/
private void upgradeToV8(SQLiteDatabase db) {
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_LOCKED
+ " INTEGER NOT NULL DEFAULT 0");
Log.d(TAG, "Database upgraded to version 8 (note lock support added)");
} catch (Exception e) {
Log.e(TAG, "Failed to upgrade to version 8, column may already exist", e);
}
}
}
}

@ -26,98 +26,44 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.AttachmentColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* ContentProvider
* CRUD使UriMatcherURI
* ContentResolver
*/
public class NotesProvider extends ContentProvider {
/**
* UriURI
*/
private static final UriMatcher mMatcher;
/**
*
*/
private NotesDatabaseHelper mHelper;
/**
*
*/
private static final String TAG = "NotesProvider";
/**
* URI
*/
private static final int URI_NOTE = 1;
/**
* URI
*/
private static final int URI_NOTE_ITEM = 2;
/**
* URI
*/
private static final int URI_DATA = 3;
/**
* URI
*/
private static final int URI_DATA_ITEM = 4;
/**
* URI
*/
private static final int URI_SEARCH = 5;
/**
* URI
*/
private static final int URI_SEARCH_SUGGEST = 6;
/**
* URI
*/
private static final int URI_ATTACHMENT = 7;
/**
* URI
*/
private static final int URI_ATTACHMENT_ITEM = 8;
/**
* UriMatcher
*/
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 添加URI匹配规则
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE); // content://micode_notes/note
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM); // content://micode_notes/note/1
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA); // content://micode_notes/data
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM); // content://micode_notes/data/1
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH); // content://micode_notes/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); // 带参数的搜索建议
mMatcher.addURI(Notes.AUTHORITY, "attachment", URI_ATTACHMENT); // content://micode_notes/attachment
mMatcher.addURI(Notes.AUTHORITY, "attachment/#", URI_ATTACHMENT_ITEM); // content://micode_notes/attachment/1
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 + ","
@ -127,35 +73,18 @@ public class NotesProvider extends ContentProvider {
+ "'" + 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_FOLDER
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
/**
* ContentProvider
* @return
*/
@Override
public boolean onCreate() {
// 获取数据库帮助类实例
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
/**
*
* @param uri URI
* @param projection
* @param selection
* @param selectionArgs
* @param sortOrder
* @return
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
@ -181,15 +110,6 @@ public class NotesProvider extends ContentProvider {
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_ATTACHMENT:
c = db.query(TABLE.ATTACHMENT, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.ATTACHMENT, projection, AttachmentColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
if (sortOrder != null || projection != null) {
@ -212,12 +132,8 @@ public class NotesProvider extends ContentProvider {
try {
searchString = String.format("%%%s%%", searchString);
Cursor rawCursor = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
// 包装Cursor以处理HTML内容
if (rawCursor != null) {
c = new HtmlCursorWrapper(rawCursor);
}
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
@ -231,246 +147,6 @@ public class NotesProvider extends ContentProvider {
return c;
}
/**
* CursorHTML
* HTML
*/
private class HtmlCursorWrapper implements Cursor {
private Cursor mCursor;
public HtmlCursorWrapper(Cursor cursor) {
mCursor = cursor;
}
@Override
public int getCount() {
return mCursor.getCount();
}
@Override
public int getPosition() {
return mCursor.getPosition();
}
@Override
public boolean move(int offset) {
return mCursor.move(offset);
}
@Override
public boolean moveToPosition(int position) {
return mCursor.moveToPosition(position);
}
@Override
public boolean moveToFirst() {
return mCursor.moveToFirst();
}
@Override
public boolean moveToLast() {
return mCursor.moveToLast();
}
@Override
public boolean moveToNext() {
return mCursor.moveToNext();
}
@Override
public boolean moveToPrevious() {
return mCursor.moveToPrevious();
}
@Override
public boolean isFirst() {
return mCursor.isFirst();
}
@Override
public boolean isLast() {
return mCursor.isLast();
}
@Override
public boolean isBeforeFirst() {
return mCursor.isBeforeFirst();
}
@Override
public boolean isAfterLast() {
return mCursor.isAfterLast();
}
@Override
public int getColumnIndex(String columnName) {
return mCursor.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mCursor.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mCursor.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mCursor.getColumnNames();
}
@Override
public int getColumnCount() {
return mCursor.getColumnCount();
}
@Override
public byte[] getBlob(int columnIndex) {
return mCursor.getBlob(columnIndex);
}
@Override
public String getString(int columnIndex) {
String value = mCursor.getString(columnIndex);
// 处理搜索结果中的HTML内容
if (value != null && (
columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1) ||
columnIndex == mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2))
) {
// 将HTML转换为普通文本
return Html.fromHtml(value).toString();
}
return value;
}
@Override
public short getShort(int columnIndex) {
return mCursor.getShort(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mCursor.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mCursor.getLong(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mCursor.getFloat(columnIndex);
}
@Override
public double getDouble(int columnIndex) {
return mCursor.getDouble(columnIndex);
}
@Override
public int getType(int columnIndex) {
return mCursor.getType(columnIndex);
}
@Override
public boolean isNull(int columnIndex) {
return mCursor.isNull(columnIndex);
}
@Override
public void deactivate() {
mCursor.deactivate();
}
@Override
public boolean requery() {
return mCursor.requery();
}
@Override
public void close() {
mCursor.close();
}
@Override
public boolean isClosed() {
return mCursor.isClosed();
}
@Override
public void registerContentObserver(android.database.ContentObserver observer) {
mCursor.registerContentObserver(observer);
}
@Override
public void unregisterContentObserver(android.database.ContentObserver observer) {
mCursor.unregisterContentObserver(observer);
}
@Override
public void registerDataSetObserver(android.database.DataSetObserver observer) {
mCursor.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(android.database.DataSetObserver observer) {
mCursor.unregisterDataSetObserver(observer);
}
@Override
public void setNotificationUri(android.content.ContentResolver cr, Uri uri) {
mCursor.setNotificationUri(cr, uri);
}
@Override
public Uri getNotificationUri() {
return mCursor.getNotificationUri();
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mCursor.getWantsAllOnMoveCalls();
}
@Override
public void setExtras(Bundle extras) {
mCursor.setExtras(extras);
}
@Override
public Bundle getExtras() {
return mCursor.getExtras();
}
@Override
public Bundle respond(Bundle extras) {
return mCursor.respond(extras);
}
@Override
public void copyStringToBuffer(int columnIndex, android.database.CharArrayBuffer buffer) {
String value = getString(columnIndex);
if (value != null) {
char[] data = value.toCharArray();
if (buffer.data == null || buffer.data.length < data.length) {
buffer.data = new char[data.length];
}
System.arraycopy(data, 0, buffer.data, 0, data.length);
buffer.sizeCopied = data.length;
}
}
}
/**
*
* @param uri URI
* @param values
* @return URI
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
@ -487,14 +163,6 @@ public class NotesProvider extends ContentProvider {
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
case URI_ATTACHMENT:
if (values.containsKey(AttachmentColumns.NOTE_ID)) {
noteId = values.getAsLong(AttachmentColumns.NOTE_ID);
} else {
Log.d(TAG, "Wrong attachment format without note id:" + values.toString());
}
insertedId = db.insert(TABLE.ATTACHMENT, null, values);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -513,13 +181,6 @@ public class NotesProvider extends ContentProvider {
return ContentUris.withAppendedId(uri, insertedId);
}
/**
*
* @param uri URI
* @param selection
* @param selectionArgs
* @return
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
@ -554,14 +215,6 @@ public class NotesProvider extends ContentProvider {
DataColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
deleteData = true;
break;
case URI_ATTACHMENT:
count = db.delete(TABLE.ATTACHMENT, selection, selectionArgs);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
count = db.delete(TABLE.ATTACHMENT,
Notes.AttachmentColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -574,14 +227,6 @@ public class NotesProvider extends ContentProvider {
return count;
}
/**
*
* @param uri URI
* @param values
* @param selection
* @param selectionArgs
* @return
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
@ -609,14 +254,6 @@ public class NotesProvider extends ContentProvider {
+ parseSelection(selection), selectionArgs);
updateData = true;
break;
case URI_ATTACHMENT:
count = db.update(TABLE.ATTACHMENT, values, selection, selectionArgs);
break;
case URI_ATTACHMENT_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.ATTACHMENT, values, "id=" + id
+ parseSelection(selection), selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -630,21 +267,10 @@ public class NotesProvider extends ContentProvider {
return count;
}
/**
*
* @param selection
* @return
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
/**
*
* @param id ID
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -670,11 +296,6 @@ public class NotesProvider extends ContentProvider {
mHelper.getWritableDatabase().execSQL(sql.toString());
}
/**
* MIME
* @param uri URI
* @return MIME
*/
@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub

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

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

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

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

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

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

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

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

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

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

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

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

@ -34,38 +34,12 @@ import net.micode.notes.data.Notes.TextNote;
import java.util.ArrayList;
/**
* Note -
*
*
*
* ContentResolverContentProvider
*
* @author MiCode Open Source Community
*/
public class Note {
/**
*
*/
private ContentValues mNoteDiffValues;
/**
*
*/
private NoteData mNoteData;
/**
*
*/
private static final String TAG = "Note";
/**
* ID
*
* ID
*
*
* @param context ContentResolver
* @param folderId ID
* @return ID
* @throws IllegalStateException
* 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
@ -91,92 +65,41 @@ public class Note {
return noteId;
}
/**
*
*
*
*/
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
/**
*
*
* @param key
* @param value
*/
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
*
* @param key
* @param value
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
/**
* ID
*
* @param id ID
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
/**
* ID
*
* @return ID
*/
public long getTextDataId() {
return mNoteData.mTextDataId;
}
/**
* ID
*
* @param id ID
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
/**
*
*
* @param key
* @param value
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
/**
*
*
* @return truefalse
*/
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
/**
*
*
* @param context ContentResolver
* @param noteId ID
* @return truefalse
* @throws IllegalArgumentException ID
*/
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
@ -207,43 +130,17 @@ public class Note {
return true;
}
/**
* NoteData -
*
*
* ContentResolver
*/
private class NoteData {
/**
* ID
*/
private long mTextDataId;
/**
*
*/
private ContentValues mTextDataValues;
/**
* ID
*/
private long mCallDataId;
/**
*
*/
private ContentValues mCallDataValues;
/**
*
*/
private static final String TAG = "NoteData";
/**
*
*
*
*/
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
@ -251,21 +148,10 @@ public class Note {
mCallDataId = 0;
}
/**
*
*
* @return truefalse
*/
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
/**
* ID
*
* @param id ID
* @throws IllegalArgumentException ID
*/
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
@ -273,12 +159,6 @@ public class Note {
mTextDataId = id;
}
/**
* ID
*
* @param id ID
* @throws IllegalArgumentException ID
*/
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
@ -286,38 +166,18 @@ public class Note {
mCallDataId = id;
}
/**
*
*
* @param key
* @param value
*/
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
*
* @param key
* @param value
*/
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
* ContentResolver
*
* @param context ContentResolver
* @param noteId ID
* @return URInull
* @throws IllegalArgumentException ID
*/
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety
@ -387,8 +247,7 @@ public class Note {
return null;
}
}
// 如果只有插入操作(没有更新操作),也返回成功
return ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
return null;
}
}
}

@ -29,83 +29,37 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* WorkingNote - 使
*
* Note便
*
*
*
* @author MiCode Open Source Community
*/
public class WorkingNote {
/**
* Note
*/
// Note for the working note
private Note mNote;
/**
*
*/
private AttachmentManager mAttachmentManager;
/**
* ID
*/
// Note Id
private long mNoteId;
/**
*
*/
// Note content
private String mContent;
/**
*
*/
// Note mode
private int mMode;
/**
*
*/
private long mAlertDate;
/**
*
*/
private long mModifiedDate;
/**
* ID
*/
private int mBgColorId;
/**
* ID
*/
private int mWidgetId;
/**
*
*/
private int mWidgetType;
/**
* ID
*/
private long mFolderId;
/**
*
*/
private Context mContext;
/**
*
*/
private static final String TAG = "WorkingNote";
/**
*
*/
private boolean mIsDeleted;
/**
*
*/
private NoteSettingChangedListener mNoteSettingStatusListener;
public static final String[] DATA_PROJECTION = new String[] {
@ -154,7 +108,6 @@ public class WorkingNote {
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
@ -168,7 +121,6 @@ public class WorkingNote {
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
loadNote();
}
@ -390,61 +342,6 @@ public class WorkingNote {
return mWidgetType;
}
/**
*
* @param type Notes.ATTACHMENT_TYPE_GALLERY Notes.ATTACHMENT_TYPE_CAMERA
* @param sourceFile
* @return ID-1
*/
public long addAttachment(int type, File sourceFile) {
if (mAttachmentManager == null || mNoteId <= 0) {
return -1;
}
return mAttachmentManager.addAttachment(mNoteId, type, sourceFile);
}
/**
*
* @param attachmentId ID
* @return
*/
public boolean deleteAttachment(long attachmentId) {
if (mAttachmentManager == null) {
return false;
}
return mAttachmentManager.deleteAttachment(attachmentId);
}
/**
*
* @return
*/
public int deleteAllAttachments() {
if (mAttachmentManager == null) {
return 0;
}
return mAttachmentManager.deleteAttachmentsByNoteId(mNoteId);
}
/**
*
* @return
*/
public List<AttachmentManager.Attachment> getAttachments() {
if (mAttachmentManager == null) {
return new ArrayList<>();
}
return mAttachmentManager.getAttachmentsByNoteId(mNoteId);
}
/**
*
* @return
*/
public AttachmentManager getAttachmentManager() {
return mAttachmentManager;
}
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed

@ -1,164 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.security;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
*
* 便
*/
public class PasswordManager {
private static final String TAG = "PasswordManager";
private static final String PREF_PASSWORD_HASH = "pref_password_hash";
private static final String PREF_PASSWORD_SET = "pref_password_set";
private static final String PREF_NAME = "notes_preferences";
/**
*
* @param context
* @return truefalse
*/
public static boolean isPasswordSet(Context context) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return sp.getBoolean(PREF_PASSWORD_SET, false);
} catch (Exception e) {
Log.e(TAG, "Error checking password status", e);
return false;
}
}
/**
*
* @param context
* @param inputPassword
* @return truefalse
*/
public static boolean verifyPassword(Context context, String inputPassword) {
if (inputPassword == null || inputPassword.isEmpty()) {
Log.w(TAG, "Empty password provided for verification");
return false;
}
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String storedHash = sp.getString(PREF_PASSWORD_HASH, "");
String inputHash = hashPassword(inputPassword);
boolean result = storedHash.equals(inputHash);
if (!result) {
Log.w(TAG, "Password verification failed");
}
return result;
} catch (Exception e) {
Log.e(TAG, "Error verifying password", e);
return false;
}
}
/**
*
* @param context
* @param password
* @return truefalse
*/
public static boolean setPassword(Context context, String password) {
if (password == null || password.isEmpty()) {
Log.w(TAG, "Empty password provided");
return false;
}
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(PREF_PASSWORD_HASH, hashPassword(password));
editor.putBoolean(PREF_PASSWORD_SET, true);
boolean result = editor.commit();
if (result) {
Log.d(TAG, "Password set successfully");
} else {
Log.e(TAG, "Failed to set password");
}
return result;
} catch (Exception e) {
Log.e(TAG, "Error setting password", e);
return false;
}
}
/**
*
* @param context
* @return truefalse
*/
public static boolean clearPassword(Context context) {
try {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove(PREF_PASSWORD_HASH);
editor.putBoolean(PREF_PASSWORD_SET, false);
boolean result = editor.commit();
if (result) {
Log.d(TAG, "Password cleared successfully");
} else {
Log.e(TAG, "Failed to clear password");
}
return result;
} catch (Exception e) {
Log.e(TAG, "Error clearing password", e);
return false;
}
}
/**
* 使SHA-256
* @param password
* @return
*/
private static String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "Hash algorithm not found", e);
return "";
}
}
}

@ -35,10 +35,10 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class BackupUtils {
private static final String TAG = "BackupUtils";
// 单例模式实现
// Singleton stuff
private static BackupUtils sInstance;
public static synchronized BackupUtils getInstance(Context context) {
@ -49,45 +49,43 @@ public class BackupUtils {
}
/**
* /
* Following states are signs to represents backup or restore
* status
*/
public static final int STATE_SD_CARD_UNMOUONTED = 0; // SD卡未挂载
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; // 备份文件不存在
public static final int STATE_DATA_DESTROIED = 2; // 数据被破坏
public static final int STATE_SYSTEM_ERROR = 3; // 系统错误
public static final int STATE_SUCCESS = 4; // 操作成功
private TextExport mTextExport; // 文本导出处理器
// Currently, the sdcard is not mounted
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// The backup file not exist
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// The data is not well formated, may be changed by other programs
public static final int STATE_DATA_DESTROIED = 2;
// Some run-time exception which causes restore or backup fails
public static final int STATE_SYSTEM_ERROR = 3;
// Backup or restore success
public static final int STATE_SUCCESS = 4;
private TextExport mTextExport;
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
// 检查外部存储是否可用
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
// 导出到文本文件的主入口方法
public int exportToText() {
return mTextExport.exportToText();
}
// 获取导出的文本文件名
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
// 获取导出的文本文件目录
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
*/
private static class TextExport {
// 笔记表查询字段
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
@ -95,11 +93,12 @@ public class BackupUtils {
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_ID = 0;
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
private static final int NOTE_COLUMN_SNIPPET = 2;
// 数据表查询字段
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
@ -109,54 +108,52 @@ public class BackupUtils {
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 static final int DATA_COLUMN_CONTENT = 0;
private static final int DATA_COLUMN_MIME_TYPE = 1;
private static final int DATA_COLUMN_CALL_DATE = 2;
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
// 文本格式化字符串(从资源文件加载)
private final String [] TEXT_FORMAT;
private static final int FORMAT_FOLDER_NAME = 0; // 文件夹名称格式
private static final int FORMAT_NOTE_DATE = 1; // 笔记日期格式
private static final int FORMAT_NOTE_CONTENT = 2; // 笔记内容格式
private 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; // 导出的文件目录
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];
}
/**
*
* @param folderId ID
* @param ps
* 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
folderId
}, null);
if (notesCursor != null) {
if (notesCursor.moveToFirst()) {
do {
// 输出笔记的最后修改日期
// Print note's last modified date
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 导出该笔记的具体内容
// Query data belong to this note
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
@ -166,25 +163,20 @@ public class BackupUtils {
}
/**
*
* @param noteId ID
* @param ps
* 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
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);
@ -193,17 +185,16 @@ public class BackupUtils {
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),
@ -214,8 +205,7 @@ public class BackupUtils {
}
dataCursor.close();
}
// 在笔记之间添加分隔符
// print a line separator between note
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
@ -226,102 +216,84 @@ public class BackupUtils {
}
/**
*
* @return
* 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;
}
// 使用try-with-resources自动关闭输出流
try (PrintStream ps = getExportToTextPrintStream()) {
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// First export folder and its notes
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// Print folder's name
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// 第一部分:导出文件夹及其中的笔记
// 查询所有文件夹(排除回收站,但包含通话记录文件夹)
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 输出文件夹名称
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
// 导出该文件夹下的所有笔记
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
}
folderCursor.close();
}
// Export notes in root's folder
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=0", null, null);
// 第二部分:导出根目录下的笔记(不属于任何文件夹的笔记)
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))));
// 导出该笔记的内容
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// Query data belong to this note
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
return STATE_SUCCESS;
} catch (Exception e) {
Log.e(TAG, "Export text failed: " + e.getMessage());
return STATE_SYSTEM_ERROR;
noteCursor.close();
}
ps.close();
return STATE_SUCCESS;
}
/**
*
* @return PrintStream null
* Get a print stream pointed to the file {@generateExportedTextFile}
*/
private PrintStream getExportToTextPrintStream() {
// 生成文件名(基于当前时间)
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
// 记录文件名和目录
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(file);
@ -338,20 +310,13 @@ public class BackupUtils {
}
/**
* SD
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return Filenull
* Generate the text file to store imported data
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
// 构建完整文件路径
StringBuilder sb = new StringBuilder();
sb.append(Environment.getExternalStorageDirectory()); // SD卡根目录
sb.append(context.getString(filePathResId)); // 相对路径
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
// 构建完整文件名(包含时间戳)
sb.append(context.getString(
fileNameFormatResId,
DateFormat.format(context.getString(R.string.format_date_ymd),
@ -359,11 +324,9 @@ public class BackupUtils {
File file = new File(sb.toString());
try {
// 创建目录(如果不存在)
if (!filedir.exists()) {
filedir.mkdir();
}
// 创建文件(如果不存在)
if (!file.exists()) {
file.createNewFile();
}
@ -377,3 +340,5 @@ public class BackupUtils {
return null;
}
}

@ -32,18 +32,11 @@ import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class DataUtils {
public static final String TAG = "DataUtils";
/**
*
* @param resolver ContentResolver
* @param ids ID
* @return
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
@ -54,21 +47,17 @@ public class DataUtils {
return true;
}
// 构建批量删除操作列表
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root"); // 禁止删除根文件夹
Log.e(TAG, "Don't delete system folder root");
continue;
}
// 为每个笔记ID创建删除操作
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
// 执行批量操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
@ -83,66 +72,31 @@ public class DataUtils {
return false;
}
/**
*
* @param resolver ContentResolver
* @param id ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新的父文件夹ID
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹ID
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已本地修改
// 更新笔记记录
values.put(NoteColumns.PARENT_ID, desFolderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
* @param resolver ContentResolver
* @param ids ID
* @param folderId ID
* @return
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
// 先查询所有笔记的当前parent_id用于记录原始文件夹
HashMap<Long, Long> noteToParentMap = new HashMap<>();
for (long id : ids) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
new String[]{NoteColumns.PARENT_ID}, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
noteToParentMap.put(id, cursor.getLong(0));
}
cursor.close();
}
}
// 构建批量更新操作列表
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId); // 更新父文件夹ID
// 记录原始父文件夹ID如果查询到的话
Long originParentId = noteToParentMap.get(id);
if (originParentId != null) {
builder.withValue(NoteColumns.ORIGIN_PARENT_ID, originParentId);
}
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为已本地修改
builder.withValue(NoteColumns.PARENT_ID, folderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
// 执行批量操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
@ -158,126 +112,92 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @return
* Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}}
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询类型为文件夹且不在回收站中的记录数量
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLDER)},
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
null);
int count = 0;
if(cursor != null) {
if(cursor.moveToFirst() && cursor.getColumnCount() > 0) {
count = cursor.getInt(0); // 获取计数结果
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
cursor.close();
}
}
cursor.close();
}
return count;
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @param type
* @return
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
// 查询指定ID和类型且不在回收站中的笔记
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
new String [] {String.valueOf(type)},
null);
boolean exists = false;
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) { // 如果查询结果数量大于0则表示存在
exists = true;
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
}
return exists;
return exist;
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 根据笔记ID直接查询
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exists = false;
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exists = true;
exist = true;
}
cursor.close();
}
return exists;
return exist;
}
/**
* ID
* @param resolver ContentResolver
* @param dataId ID
* @return
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
// 根据数据项ID直接查询
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exists = false;
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) {
exists = true;
exist = true;
}
cursor.close();
}
return exists;
return exist;
}
/**
*
* @param resolver ContentResolver
* @param name
* @return
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
// 查询类型为文件夹、不在回收站中且名称匹配的记录
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER +
" AND " + NoteColumns.SNIPPET + "=?",
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exists = false;
boolean exist = false;
if(cursor != null) {
if(cursor.getCount() > 0) {
exists = true;
exist = true;
}
cursor.close();
}
return exists;
return exist;
}
/**
*
* @param resolver ContentResolver
* @param folderId ID
* @return
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
// 查询指定文件夹下所有笔记的小部件信息
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
@ -289,12 +209,13 @@ public class DataUtils {
if (c.moveToFirst()) {
set = new HashSet<AppWidgetAttribute>();
do {
AppWidgetAttribute widget = new AppWidgetAttribute();
// 确保列索引有效
if (c.getColumnCount() > 1) {
widget.widgetId = c.getInt(0); // 小部件ID
widget.widgetType = c.getInt(1); // 小部件类型
try {
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0);
widget.widgetType = c.getInt(1);
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
}
} while (c.moveToNext());
}
@ -303,14 +224,7 @@ public class DataUtils {
return set;
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
// 查询通话记录类型数据项中的电话号码
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
@ -319,9 +233,9 @@ public class DataUtils {
if (cursor != null && cursor.moveToFirst()) {
try {
if (cursor.getColumnCount() > 0) {
return cursor.getString(0); // 返回电话号码
}
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
cursor.close();
}
@ -329,40 +243,28 @@ public class DataUtils {
return "";
}
/**
* ID
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return ID0
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
// 查询指定电话号码和通话日期的通话记录
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)" ,
+ CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
if (cursor != null) {
if (cursor.moveToFirst() && cursor.getColumnCount() > 0) {
return cursor.getLong(0); // 返回笔记ID
if (cursor.moveToFirst()) {
try {
return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
}
cursor.close();
}
return 0;
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
// 查询指定笔记的摘要
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
@ -372,7 +274,7 @@ public class DataUtils {
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0); // 获取摘要内容
snippet = cursor.getString(0);
}
cursor.close();
return snippet;
@ -380,61 +282,14 @@ public class DataUtils {
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
*
* @param snippet
* @return
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim(); // 去除首尾空格
int index = snippet.indexOf('\n'); // 查找换行符位置
snippet = snippet.trim();
int index = snippet.indexOf('\n');
if (index != -1) {
snippet = snippet.substring(0, index); // 只保留第一行
snippet = snippet.substring(0, index);
}
}
return snippet;
}
/**
* 便
* @param resolver ContentResolver
* @param ids 便ID
* @param lock truefalse
* @return
*/
public static boolean batchSetLockStatus(ContentResolver resolver, HashSet<Long> ids, boolean lock) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
if (ids.size() == 0) {
Log.d(TAG, "no id is in hashset");
return true;
}
// 构建批量更新操作列表
ArrayList<ContentProviderOperation> operationList = new ArrayList<>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.IS_LOCKED, lock ? 1 : 0);
operationList.add(builder.build());
}
try {
// 执行批量操作
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "set lock status failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
}

@ -1,111 +0,0 @@
/*
* Copyright (c) 2026, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
/**
*
*/
public class ElderModeUtils {
/**
*
*/
private static final float ELDER_MODE_FONT_SCALE = 1.1f;
/**
*
*
* @param context
* @return
*/
public static boolean isElderModeEnabled(Context context) {
// 使用默认的SharedPreferences因为CheckBoxPreference会自动保存到这里
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
return preferences.getBoolean(NotesPreferenceActivity.PREFERENCE_ELDER_MODE_KEY, false);
}
/**
* View
*
* @param context
* @param view View
*/
public static void applyElderMode(Context context, View view) {
if (!isElderModeEnabled(context)) {
return;
}
// 只对用户输入的内容应用字体增大,系统信息保持不变
if (view instanceof TextView) {
TextView textView = (TextView) view;
int id = textView.getId();
// 只增大用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() * ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
applyElderMode(context, viewGroup.getChildAt(i));
}
}
}
/**
*
*
* @param context
* @param view View
*/
public static void clearElderMode(Context context, View view) {
// 移除这个检查,因为清除操作应该在老年人模式禁用时执行
// if (!isElderModeEnabled(context)) {
// return;
// }
// 只对用户输入的内容应用字体恢复,系统信息保持不变
if (view instanceof TextView) {
TextView textView = (TextView) view;
int id = textView.getId();
// 只恢复用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() / ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
clearElderMode(context, viewGroup.getChildAt(i));
}
}
}
}

@ -17,72 +17,97 @@
package net.micode.notes.tool;
public class GTaskStringUtils {
// GTasks API JSON字段常量定义
// 动作相关的字段
public final static String GTASK_JSON_ACTION_ID = "action_id"; // 动作ID标识
public final static String GTASK_JSON_ACTION_LIST = "action_list"; // 动作列表
public final static String GTASK_JSON_ACTION_TYPE = "action_type"; // 动作类型
// 动作类型枚举
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create"; // 创建动作
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all"; // 获取所有数据动作
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move"; // 移动动作
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; // 更新动作
// 实体相关字段
public final static String GTASK_JSON_CREATOR_ID = "creator_id"; // 创建者ID
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity"; // 子实体
public final static String GTASK_JSON_CLIENT_VERSION = "client_version"; // 客户端版本号
public final static String GTASK_JSON_COMPLETED = "completed"; // 完成状态
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id"; // 当前列表ID
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认列表ID
public final static String GTASK_JSON_DELETED = "deleted"; // 删除状态标记
public final static String GTASK_JSON_DEST_LIST = "dest_list"; // 目标列表
public final static String GTASK_JSON_DEST_PARENT = "dest_parent"; // 目标父节点
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父节点类型
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta"; // 实体增量变化
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型
public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 是否获取已删除项
public final static String GTASK_JSON_ID = "id"; // 实体ID
public final static String GTASK_JSON_INDEX = "index"; // 索引位置
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point"; // 最新同步点
// 列表相关字段
public final static String GTASK_JSON_LIST_ID = "list_id"; // 列表ID
public final static String GTASK_JSON_LISTS = "lists"; // 列表集合
public final static String GTASK_JSON_NAME = "name"; // 实体名称
public final static String GTASK_JSON_NEW_ID = "new_id"; // 新ID用于ID映射
public final static String GTASK_JSON_NOTES = "notes"; // 笔记集合
// 层级关系字段
public final static String GTASK_JSON_PARENT_ID = "parent_id"; // 父节点ID
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id"; // 前一个兄弟节点ID
// 响应相关字段
public final static String GTASK_JSON_RESULTS = "results"; // 操作结果集
public final static String GTASK_JSON_SOURCE_LIST = "source_list"; // 源列表
// 任务相关字段
public final static String GTASK_JSON_TASKS = "tasks"; // 任务集合
public final static String GTASK_JSON_TYPE = "type"; // 实体类型标识
// 实体类型常量
public final static String GTASK_JSON_TYPE_GROUP = "GROUP"; // 分组/文件夹类型
public final static String GTASK_JSON_TYPE_TASK = "TASK"; // 任务类型
public final static String GTASK_JSON_USER = "user"; // 用户信息
// 小米便签同步相关的文件夹命名前缀和常量
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // 小米便签文件夹前缀
public final static String FOLDER_DEFAULT = "Default"; // 默认文件夹名称
public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话记录文件夹名称
public final static String FOLDER_META = "METADATA"; // 元数据文件夹名称
// 元数据相关的常量
public final static String META_HEAD_GTASK_ID = "meta_gid"; // Google Task ID的元数据头
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"; // 元数据笔记名称(用户不应修改或删除)
public final static String GTASK_JSON_ACTION_ID = "action_id";
public final static String GTASK_JSON_ACTION_LIST = "action_list";
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
public final static String GTASK_JSON_COMPLETED = "completed";
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
public final static String GTASK_JSON_DELETED = "deleted";
public final static String GTASK_JSON_DEST_LIST = "dest_list";
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
public final static String GTASK_JSON_ID = "id";
public final static String GTASK_JSON_INDEX = "index";
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
public final static String GTASK_JSON_LIST_ID = "list_id";
public final static String GTASK_JSON_LISTS = "lists";
public final static String GTASK_JSON_NAME = "name";
public final static String GTASK_JSON_NEW_ID = "new_id";
public final static String GTASK_JSON_NOTES = "notes";
public final static String GTASK_JSON_PARENT_ID = "parent_id";
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
public final static String GTASK_JSON_RESULTS = "results";
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
public final static String GTASK_JSON_TASKS = "tasks";
public final static String GTASK_JSON_TYPE = "type";
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
public final static String GTASK_JSON_TYPE_TASK = "TASK";
public final static String GTASK_JSON_USER = "user";
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
public final static String FOLDER_DEFAULT = "Default";
public final static String FOLDER_CALL_NOTE = "Call_Note";
public final static String FOLDER_META = "METADATA";
public final static String META_HEAD_GTASK_ID = "meta_gid";
public final static String META_HEAD_NOTE = "meta_note";
public final static String META_HEAD_DATA = "meta_data";
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -1,359 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.account.AccountManager;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.model.Note;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* NoteSyncUtils -
*
* JSON
*
*/
public class NoteSyncUtils {
private static final String TAG = "NoteSyncUtils";
/**
* JSON
* @param context
* @return JSONnull
*/
public static String localNotesToJson(Context context) {
try {
JSONObject root = new JSONObject();
// 添加用户信息
String username = AccountManager.getCurrentUser(context);
root.put("user", username);
// 添加同步时间
root.put("sync_time", System.currentTimeMillis());
// 添加笔记列表
JSONArray notesArray = new JSONArray();
// 查询所有笔记
ContentResolver resolver = context.getContentResolver();
Cursor noteCursor = resolver.query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER)},
null
);
if (noteCursor != null) {
while (noteCursor.moveToNext()) {
long noteId = noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.ID));
JSONObject noteObj = createNoteJson(context, noteId, noteCursor);
if (noteObj != null) {
notesArray.put(noteObj);
}
}
noteCursor.close();
}
root.put("notes", notesArray);
return root.toString();
} catch (Exception e) {
Log.e(TAG, "Failed to convert local notes to JSON", e);
return null;
}
}
/**
* JSON
* @param context
* @param noteId ID
* @param noteCursor
* @return JSONnull
*/
private static JSONObject createNoteJson(Context context, long noteId, Cursor noteCursor) {
try {
JSONObject noteObj = new JSONObject();
// 基本信息
noteObj.put("id", noteId);
noteObj.put("parent_id", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.PARENT_ID)));
noteObj.put("created_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.CREATED_DATE)));
noteObj.put("modified_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.MODIFIED_DATE)));
noteObj.put("alert_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.ALERTED_DATE)));
noteObj.put("snippet", noteCursor.getString(noteCursor.getColumnIndex(NoteColumns.SNIPPET)));
noteObj.put("bg_color_id", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.BG_COLOR_ID)));
noteObj.put("has_attachment", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.HAS_ATTACHMENT)));
noteObj.put("is_locked", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.IS_LOCKED)));
// 数据内容
JSONObject dataObj = new JSONObject();
// 查询笔记数据
ContentResolver resolver = context.getContentResolver();
Cursor dataCursor = resolver.query(
Notes.CONTENT_DATA_URI,
null,
DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(noteId)},
null
);
if (dataCursor != null) {
while (dataCursor.moveToNext()) {
String mimeType = dataCursor.getString(dataCursor.getColumnIndex(DataColumns.MIME_TYPE));
if (TextNote.CONTENT_ITEM_TYPE.equals(mimeType)) {
// 文本笔记
dataObj.put("type", "text");
dataObj.put("content", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.CONTENT)));
dataObj.put("mode", dataCursor.getInt(dataCursor.getColumnIndex(TextNote.MODE)));
} else if (CallNote.CONTENT_ITEM_TYPE.equals(mimeType)) {
// 通话笔记
dataObj.put("type", "call");
dataObj.put("content", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.CONTENT)));
dataObj.put("call_date", dataCursor.getLong(dataCursor.getColumnIndex(CallNote.CALL_DATE)));
dataObj.put("phone_number", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.DATA3)));
}
}
dataCursor.close();
}
noteObj.put("data", dataObj);
// 附件信息
JSONArray attachmentsArray = new JSONArray();
AttachmentManager attachmentManager = new AttachmentManager(context);
List<AttachmentManager.Attachment> attachments = attachmentManager.getAttachmentsByNoteId(noteId);
for (AttachmentManager.Attachment attachment : attachments) {
JSONObject attachmentObj = new JSONObject();
attachmentObj.put("id", attachment.id);
attachmentObj.put("type", attachment.type);
attachmentObj.put("file_path", attachment.filePath);
attachmentObj.put("created_time", attachment.createdTime);
attachmentsArray.put(attachmentObj);
}
noteObj.put("attachments", attachmentsArray);
return noteObj;
} catch (Exception e) {
Log.e(TAG, "Failed to create note JSON", e);
return null;
}
}
/**
* JSON
* @param context
* @param jsonContent JSON
* @return
*/
public static boolean jsonToLocalNotes(Context context, String jsonContent) {
try {
JSONObject root = new JSONObject(jsonContent);
JSONArray notesArray = root.getJSONArray("notes");
// 获取本地现有笔记ID集合
Map<Long, Boolean> localNoteIds = getLocalNoteIds(context);
// 处理每个云端笔记
for (int i = 0; i < notesArray.length(); i++) {
JSONObject noteObj = notesArray.getJSONObject(i);
long noteId = noteObj.getLong("id");
// 云端有,本地无 → 在本地新增
if (!localNoteIds.containsKey(noteId)) {
createLocalNote(context, noteObj);
}
// 本地有,云端也有 → 保留本地版本,忽略云端修改
// 本地有,云端无 → 保留本地版本,不处理
}
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to convert JSON to local notes", e);
return false;
}
}
/**
* ID
* @param context
* @return ID
*/
private static Map<Long, Boolean> getLocalNoteIds(Context context) {
Map<Long, Boolean> noteIds = new HashMap<>();
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID},
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER)},
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
long noteId = cursor.getLong(0);
noteIds.put(noteId, true);
}
cursor.close();
}
return noteIds;
}
/**
*
* @param context
* @param noteObj JSON
*/
private static void createLocalNote(Context context, JSONObject noteObj) {
try {
Log.d(TAG, "Creating new note from cloud: " + noteObj.toString());
// 获取笔记基本信息
long parentId = noteObj.optLong("parent_id", Notes.ID_ROOT_FOLDER);
long createdDate = noteObj.optLong("created_date", System.currentTimeMillis());
long modifiedDate = noteObj.optLong("modified_date", System.currentTimeMillis());
long alertDate = noteObj.optLong("alert_date", 0);
String snippet = noteObj.optString("snippet", "");
int bgColorId = noteObj.optInt("bg_color_id", 0);
int hasAttachment = noteObj.optInt("has_attachment", 0);
int isLocked = noteObj.optInt("is_locked", 0);
// 获取笔记内容
JSONObject dataObj = noteObj.optJSONObject("data");
String noteType = dataObj.optString("type", "text");
// 创建新笔记
long noteId = Note.getNewNoteId(context, parentId);
Log.d(TAG, "Created new note with id: " + noteId);
// 创建Note对象
Note note = new Note();
// 设置笔记基本信息
note.setNoteValue(NoteColumns.CREATED_DATE, String.valueOf(createdDate));
note.setNoteValue(NoteColumns.MODIFIED_DATE, String.valueOf(modifiedDate));
note.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(alertDate));
note.setNoteValue(NoteColumns.SNIPPET, snippet);
note.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(bgColorId));
note.setNoteValue(NoteColumns.HAS_ATTACHMENT, String.valueOf(hasAttachment));
note.setNoteValue(NoteColumns.IS_LOCKED, String.valueOf(isLocked));
// 设置笔记内容
if ("text".equals(noteType)) {
// 文本笔记
String content = dataObj.optString("content", "");
int mode = dataObj.optInt("mode", 0);
note.setTextData(TextNote.CONTENT, content);
note.setTextData(TextNote.MODE, String.valueOf(mode));
Log.d(TAG, "Set text note content: " + content);
} else if ("call".equals(noteType)) {
// 通话笔记
String content = dataObj.optString("content", "");
long callDate = dataObj.optLong("call_date", 0);
String phoneNumber = dataObj.optString("phone_number", "");
note.setCallData(CallNote.CONTENT, content);
note.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
note.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
Log.d(TAG, "Set call note content: " + content + ", phone: " + phoneNumber);
}
// 同步笔记到数据库
boolean success = note.syncNote(context, noteId);
Log.d(TAG, "Sync note result: " + success);
if (success) {
Log.d(TAG, "Note created successfully from cloud: " + noteId);
// 恢复附件信息
JSONArray attachmentsArray = noteObj.optJSONArray("attachments");
if (attachmentsArray != null && attachmentsArray.length() > 0) {
Log.d(TAG, "Restoring " + attachmentsArray.length() + " attachments for note: " + noteId);
for (int i = 0; i < attachmentsArray.length(); i++) {
JSONObject attachmentObj = attachmentsArray.optJSONObject(i);
if (attachmentObj != null) {
// 创建附件记录附件文件需要从OSS下载
String filePath = attachmentObj.optString("file_path", "");
int type = attachmentObj.optInt("type", 0);
long createdTime = attachmentObj.optLong("created_time", System.currentTimeMillis());
// 注意这里只记录附件元数据实际的文件下载由SyncManager处理
// 文件下载后会更新filePath
createAttachmentRecord(context, noteId, type, filePath, createdTime);
}
}
}
} else {
Log.e(TAG, "Failed to sync note to local database: " + noteId);
}
} catch (Exception e) {
Log.e(TAG, "Failed to create local note", e);
}
}
/**
*
* @param context
* @param noteId ID
* @param type
* @param filePath
* @param createdTime
*/
private static void createAttachmentRecord(Context context, long noteId, int type, String filePath, long createdTime) {
try {
android.content.ContentValues values = new android.content.ContentValues();
values.put(Notes.AttachmentColumns.NOTE_ID, noteId);
values.put(Notes.AttachmentColumns.TYPE, type);
values.put(Notes.AttachmentColumns.FILE_PATH, filePath);
values.put(Notes.AttachmentColumns.CREATED_TIME, createdTime);
android.net.Uri uri = context.getContentResolver().insert(Notes.CONTENT_ATTACHMENT_URI, values);
if (uri != null) {
Log.d(TAG, "Created attachment record: " + uri + " for note: " + noteId);
} else {
Log.e(TAG, "Failed to create attachment record for note: " + noteId);
}
} catch (Exception e) {
Log.e(TAG, "Failed to create attachment record", e);
}
}
}

@ -1,148 +0,0 @@
package net.micode.notes.tool;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class PermissionHelper {
/**
*
* @param context
* @param permission
* @return
*/
public static boolean isPermissionGranted(Context context, String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
/**
*
* @param context
* @param permissions
* @return
*/
public static boolean arePermissionsGranted(Context context, String[] permissions) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
for (String permission : permissions) {
if (!isPermissionGranted(context, permission)) {
return false;
}
}
return true;
}
/**
*
* @param activity
* @param permissions
* @param requestCode
*/
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return;
}
ActivityCompat.requestPermissions(activity, permissions, requestCode);
}
/**
*
* @param activity
* @param permission
* @param requestCode
*/
public static void requestPermission(Activity activity, String permission, int requestCode) {
requestPermissions(activity, new String[]{permission}, requestCode);
}
/**
*
* @param requestCode
* @param permissions
* @param grantResults
* @param listener
*/
public static void handlePermissionResult(int requestCode, String[] permissions, int[] grantResults,
OnPermissionResultListener listener) {
if (listener == null) {
return;
}
List<String> granted = new ArrayList<>();
List<String> denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(permissions[i]);
} else {
denied.add(permissions[i]);
}
}
if (denied.isEmpty()) {
listener.onAllGranted(requestCode, granted);
} else {
listener.onDenied(requestCode, granted, denied);
}
}
/**
*
* @param activity
* @param permission
* @return
*/
public static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
}
/**
*
*/
public interface OnPermissionResultListener {
/**
*
* @param requestCode
* @param grantedPermissions
*/
void onAllGranted(int requestCode, List<String> grantedPermissions);
/**
*
* @param requestCode
* @param grantedPermissions
* @param deniedPermissions
*/
void onDenied(int requestCode, List<String> grantedPermissions, List<String> deniedPermissions);
}
/**
*
*/
public static class Permissions {
public static final String CAMERA = android.Manifest.permission.CAMERA;
public static final String READ_EXTERNAL_STORAGE = android.Manifest.permission.READ_EXTERNAL_STORAGE;
public static final String WRITE_EXTERNAL_STORAGE = android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
// Android 13+ 需要单独请求媒体权限
public static final String READ_MEDIA_IMAGES = android.Manifest.permission.READ_MEDIA_IMAGES;
}
}

@ -22,240 +22,151 @@ import android.preference.PreferenceManager;
import net.micode.notes.R;
import net.micode.notes.ui.NotesPreferenceActivity;
import java.security.SecureRandom;
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 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 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 final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
*
* ID
*/
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 // 红色编辑器背景
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 // 红色标题背景
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
R.drawable.edit_title_white,
R.drawable.edit_title_green,
R.drawable.edit_title_red
};
/**
* ID
* @param id IDYELLOW, BLUE
* @return DrawableID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
* ID
* @param id IDYELLOW, BLUE
* @return DrawableID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
/**
* ID
* 使
* @param context
* @return ID
*/
public static int getDefaultBgId(Context context) {
// 检查用户是否启用了随机背景颜色功能
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
// 使用密码学安全的随机数生成器随机选择一个背景颜色
SecureRandom secureRandom = new SecureRandom();
return secureRandom.nextInt(NoteBgResources.BG_EDIT_RESOURCES.length);
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
// 使用默认的背景颜色(黄色)
return BG_DEFAULT_COLOR;
}
}
/**
*
* ID
*/
public static class NoteItemBgResources {
// 列表首项背景资源数组
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up, // 黄色列表首项背景
R.drawable.list_blue_up, // 蓝色列表首项背景
R.drawable.list_white_up, // 白色列表首项背景
R.drawable.list_green_up, // 绿色列表首项背景
R.drawable.list_red_up // 红色列表首项背景
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
R.drawable.list_white_up,
R.drawable.list_green_up,
R.drawable.list_red_up
};
// 列表中间项背景资源数组
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle, // 黄色列表中间项背景
R.drawable.list_blue_middle, // 蓝色列表中间项背景
R.drawable.list_white_middle, // 白色列表中间项背景
R.drawable.list_green_middle, // 绿色列表中间项背景
R.drawable.list_red_middle // 红色列表中间项背景
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
R.drawable.list_white_middle,
R.drawable.list_green_middle,
R.drawable.list_red_middle
};
// 列表末项背景资源数组
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down, // 黄色列表末项背景
R.drawable.list_blue_down, // 蓝色列表末项背景
R.drawable.list_white_down, // 白色列表末项背景
R.drawable.list_green_down, // 绿色列表末项背景
R.drawable.list_red_down, // 红色列表末项背景
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
R.drawable.list_white_down,
R.drawable.list_green_down,
R.drawable.list_red_down,
};
// 列表单项(只有一个项)背景资源数组
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single, // 黄色列表单项背景
R.drawable.list_blue_single, // 蓝色列表单项背景
R.drawable.list_white_single, // 白色列表单项背景
R.drawable.list_green_single, // 绿色列表单项背景
R.drawable.list_red_single // 红色列表单项背景
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
R.drawable.list_white_single,
R.drawable.list_green_single,
R.drawable.list_red_single
};
/**
* ID
* @param id ID
* @return DrawableID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
* ID
* @param id ID
* @return DrawableID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
* ID使
* @param id ID
* @return DrawableID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
* ID
* @param id ID
* @return DrawableID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
* ID使
* @return DrawableID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
/**
*
* ID
*/
public static class WidgetBgResources {
// 2x小部件背景资源数组
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow, // 黄色2x小部件背景
R.drawable.widget_2x_blue, // 蓝色2x小部件背景
R.drawable.widget_2x_white, // 白色2x小部件背景
R.drawable.widget_2x_green, // 绿色2x小部件背景
R.drawable.widget_2x_red, // 红色2x小部件背景
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
R.drawable.widget_2x_white,
R.drawable.widget_2x_green,
R.drawable.widget_2x_red,
};
/**
* 2xID
* @param id ID
* @return DrawableID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
// 4x小部件背景资源数组
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow, // 黄色4x小部件背景
R.drawable.widget_4x_blue, // 蓝色4x小部件背景
R.drawable.widget_4x_white, // 白色4x小部件背景
R.drawable.widget_4x_green, // 绿色4x小部件背景
R.drawable.widget_4x_red // 红色4x小部件背景
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
R.drawable.widget_4x_white,
R.drawable.widget_4x_green,
R.drawable.widget_4x_red
};
/**
* 4xID
* @param id ID
* @return DrawableID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
/**
*
* ID
*/
public static class TextAppearanceResources {
// 文字外观样式资源数组
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal, // 正常大小文字样式
R.style.TextAppearanceMedium, // 中等大小文字样式
R.style.TextAppearanceLarge, // 大号文字样式
R.style.TextAppearanceSuper // 超大号文字样式
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
R.style.TextAppearanceLarge,
R.style.TextAppearanceSuper
};
/**
* ID
* @param id IDTEXT_SMALL, TEXT_MEDIUM
* @return ID
*
* bughackSharedPreferences
* IDID
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: SharedPreferencesIDbug
* IDID
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE;
@ -263,10 +174,6 @@ public class ResourceParser {
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}

@ -39,27 +39,21 @@ import net.micode.notes.tool.DataUtils;
import java.io.IOException;
/**
* Activity -
* DialogInterface.OnClickListenerOnDismissListener
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private long mNoteId; // 触发闹钟的笔记ID
private String mSnippet; // 笔记内容摘要
private static final int SNIPPET_PREW_MAX_LEN = 60; // 摘要最大显示长度
MediaPlayer mPlayer; // 媒体播放器,用于播放闹钟声音
private long mNoteId;
private String mSnippet;
private static final int SNIPPET_PREW_MAX_LEN = 60;
MediaPlayer mPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 移除标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取窗口并设置标志使Activity在锁屏状态下也能显示
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
@ -67,143 +61,98 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}
// 从Intent中获取笔记ID和内容摘要
Intent intent = getIntent();
try {
// 从Intent的URI中解析出笔记IDURI格式content://xxx/note/笔记ID
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 通过笔记ID从数据库获取笔记摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 如果摘要过长截取前60个字符并添加省略号
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
// 如果解析失败打印异常并返回Activity将结束
e.printStackTrace();
return;
}
// 初始化媒体播放器
mPlayer = new MediaPlayer();
// 检查笔记是否仍然存在且可见(未被删除或移到回收站)
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog(); // 显示提醒对话框
playAlarmSound(); // 播放闹钟声音
showActionDialog();
playAlarmSound();
} else {
// 如果笔记已不存在直接结束Activity
finish();
}
}
/**
*
* @return truefalse
*/
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
/**
*
* 使
*/
private void playAlarmSound() {
// 获取系统默认的闹钟铃声URI
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 获取当前静音模式影响的音频流设置
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
// 如果静音模式影响闹钟音频流,则使用静音模式的设置
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
// 否则使用闹钟音频流
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
// 设置数据源并准备播放
mPlayer.setDataSource(this, url);
mPlayer.prepare();
mPlayer.setLooping(true); // 设置为循环播放
mPlayer.start(); // 开始播放
mPlayer.setLooping(true);
mPlayer.start();
} catch (IllegalArgumentException e) {
// TODO: 异常处理自动生成的catch块
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO: 异常处理自动生成的catch块
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO: 异常处理自动生成的catch块
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO: 异常处理自动生成的catch块
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
*
*/
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name); // 设置对话框标题为应用名称
dialog.setMessage(mSnippet); // 设置对话框内容为笔记摘要
// 确定按钮 - 关闭提醒
dialog.setTitle(R.string.app_name);
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this);
// 如果屏幕已亮,显示"进入"按钮,用于打开笔记编辑页面
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
}
// 显示对话框并设置关闭监听器
dialog.show().setOnDismissListener(this);
}
/**
*
* @param dialog
* @param which
*/
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// "进入"按钮:打开笔记编辑页面
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递笔记ID
intent.putExtra(Intent.EXTRA_UID, mNoteId);
startActivity(intent);
break;
default:
// "确定"按钮或其他按钮:不做特殊处理,对话框会自动关闭
break;
}
}
/**
*
* @param dialog
*/
public void onDismiss(DialogInterface dialog) {
stopAlarmSound(); // 停止播放闹钟声音
finish(); // 结束Activity
stopAlarmSound();
finish();
}
/**
*
*/
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop(); // 停止播放
mPlayer.release(); // 释放媒体播放器资源
mPlayer = null; // 置空引用
mPlayer.stop();
mPlayer.release();
mPlayer = null;
}
}
}

@ -27,64 +27,39 @@ import android.database.Cursor;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
* -
* BroadcastReceiver广
*/
public class AlarmInitReceiver extends BroadcastReceiver {
// 数据库查询字段投影只需要笔记ID和提醒时间
private static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE // 闹钟提醒时间
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
private static final int COLUMN_ID = 0; // 笔记ID在查询结果中的列索引
private static final int COLUMN_ALERTED_DATE = 1; // 提醒时间在查询结果中的列索引
private static final int COLUMN_ID = 0;
private static final int COLUMN_ALERTED_DATE = 1;
@Override
public void onReceive(Context context, Intent intent) {
long currentDate = System.currentTimeMillis(); // 获取当前系统时间
// 查询所有未过期的闹钟笔记:
// 1. 提醒时间大于当前时间ALERTED_DATE > ?
// 2. 笔记类型为普通笔记TYPE = Notes.TYPE_NOTE
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) }, // 将当前时间作为参数传入
new String[] { String.valueOf(currentDate) },
null);
if (c != null) {
if (c.moveToFirst()) {
do {
// 获取该笔记的提醒时间
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
// 创建指向AlarmReceiver的Intent用于在闹钟时间触发时发送广播
Intent sender = new Intent(context, AlarmReceiver.class);
// 将笔记URI附加到Intent中这样AlarmReceiver就能知道是哪个笔记的闹钟
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建PendingIntent用于在指定时间触发广播
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 获取系统AlarmManager服务
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
// 设置闹钟:
// 1. 使用RTC_WAKEUP类型即使设备休眠也会唤醒CPU并发送PendingIntent
// 2. 在alertDate时间触发
// 3. 触发pendingIntent即发送广播给AlarmReceiver
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 遍历所有查询结果
} while (c.moveToNext());
}
c.close(); // 关闭Cursor释放资源
c.close();
}
// 注意:这个方法没有处理重复闹钟的情况
// 当设备重启后所有之前设置的AlarmManager闹钟都会被清除
// 所以需要这个Receiver来重新设置所有未触发的闹钟
}
}

@ -20,28 +20,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* - AlarmManager广
* 广AlarmAlertActivity
*/
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 修改Intent的目标类为AlarmAlertActivity
// 这样当闹钟触发时会启动AlarmAlertActivity显示提醒界面
intent.setClass(context, AlarmAlertActivity.class);
// 添加FLAG_ACTIVITY_NEW_TASK标志
// 因为BroadcastReceiver不是Activity上下文需要新任务栈启动Activity
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动AlarmAlertActivity
context.startActivity(intent);
// 注意这个Intent中已经包含了笔记的URI数据
// 这个URI是在AlarmInitReceiver中设置的
// sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)))
// 所以AlarmAlertActivity可以通过getIntent().getData()获取到笔记信息
}
}

@ -1,97 +0,0 @@
package net.micode.notes.ui;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import net.micode.notes.R;
import java.io.File;
/**
*
*/
public class CameraPreviewDialogFragment extends DialogFragment {
public static final String TAG = "CameraPreviewDialogFragment";
public interface OnCameraPreviewListener {
void onConfirm(File photoFile);
void onRetake();
}
private OnCameraPreviewListener mListener;
private File mPhotoFile;
public CameraPreviewDialogFragment() {
// Required empty public constructor
}
public void setPhotoFile(File photoFile) {
mPhotoFile = photoFile;
}
public void setOnCameraPreviewListener(OnCameraPreviewListener listener) {
mListener = listener;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = new Dialog(getActivity());
dialog.setContentView(R.layout.dialog_camera_preview);
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_camera_preview, container, false);
ImageView previewImage = view.findViewById(R.id.preview_image);
Button btnRetake = view.findViewById(R.id.btn_retake);
Button btnConfirm = view.findViewById(R.id.btn_confirm);
// 加载预览图片
if (mPhotoFile != null && mPhotoFile.exists()) {
// 使用BitmapFactory解码图片避免OOM
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 降低采样率,减少内存占用
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(mPhotoFile.getAbsolutePath(), options);
if (bitmap != null) {
previewImage.setImageBitmap(bitmap);
}
}
btnRetake.setOnClickListener(v -> {
if (mListener != null) {
mListener.onRetake();
}
dismiss();
});
btnConfirm.setOnClickListener(v -> {
if (mListener != null && mPhotoFile != null) {
mListener.onConfirm(mPhotoFile);
}
dismiss();
});
return view;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
mListener = null;
mPhotoFile = null;
}
}

@ -21,121 +21,92 @@ import java.util.Calendar;
import net.micode.notes.R;
import android.content.Context;
import android.text.format.DateFormat;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
/**
*
* FrameLayoutAM/PM
*/
public class DateTimePicker extends FrameLayout {
// 常量定义
private static final boolean DEFAULT_ENABLE_STATE = true; // 默认启用状态
// 时间相关常量
private static final int HOURS_IN_HALF_DAY = 12; // 半天的小时数12小时制
private static final int HOURS_IN_ALL_DAY = 24; // 全天的小时数24小时制
private static final int DAYS_IN_ALL_WEEK = 7; // 一周的天数(用于显示一周的日期)
private static final boolean DEFAULT_ENABLE_STATE = true;
// NumberPicker的最小最大值常量
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 HOURS_IN_HALF_DAY = 12;
private static final int HOURS_IN_ALL_DAY = 24;
private static final int DAYS_IN_ALL_WEEK = 7;
private static final int DATE_SPINNER_MIN_VAL = 0;
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
// 12小时制下的小时选择器范围
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
// 分钟选择器范围
private static final int MINUT_SPINNER_MIN_VAL = 0;
private static final int MINUT_SPINNER_MAX_VAL = 59;
// AM/PM选择器范围
private static final int AMPM_SPINNER_MIN_VAL = 0;
private static final int AMPM_SPINNER_MAX_VAL = 1;
// 控件引用
private final NumberPicker mDateSpinner; // 日期选择器
private final NumberPicker mHourSpinner; // 小时选择器
private final NumberPicker mMinuteSpinner; // 分钟选择器
private final NumberPicker mAmPmSpinner; // AM/PM选择器
private final NumberPicker mDateSpinner;
private final NumberPicker mHourSpinner;
private final NumberPicker mMinuteSpinner;
private final NumberPicker mAmPmSpinner;
private Calendar mDate;
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
private boolean mIsAm;
private Calendar mDate; // 当前选择的日期时间
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK]; // 日期显示值数组
private boolean mIs24HourView;
private boolean mIsAm; // 是否为上午true=上午false=下午)
private boolean mIs24HourView; // 是否为24小时制
private boolean mIsEnabled = DEFAULT_ENABLE_STATE; // 控件是否启用
private boolean mInitialising; // 是否正在初始化
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
private OnDateTimeChangedListener mOnDateTimeChangedListener; // 日期时间变化监听器
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(); // 触发日期时间变化回调
updateDateControl();
onDateTimeChanged();
}
};
// 小时变化监听器处理复杂的时间逻辑包括12/24小时制转换和日期跨天
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false; // 标记日期是否发生变化
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
// 12小时制下的特殊处理
if (!mIs24HourView) {
// 处理从11PM到12AM或12AM到11PM的日期变化
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
// 从下午11点切换到12点日期加1天
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) {
// 从上午12点切换到11点日期减1天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
// 处理AM/PM切换当在11和12之间切换时
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm; // 切换AM/PM状态
updateAmPmControl(); // 更新AM/PM控件
mIsAm = !mIsAm;
updateAmPmControl();
}
} else {
// 24小时制下的特殊处理
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
// 从23点切换到0点日期加1天
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点日期减1天
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// 计算新的小时考虑12小时制和AM/PM
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
onDateTimeChanged(); // 触发日期时间变化回调
// 如果日期发生变化,更新年、月、日
onDateTimeChanged();
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
@ -144,30 +115,21 @@ public class DateTimePicker extends FrameLayout {
}
};
// 分钟变化监听器(处理分钟滚动到边界时的日期变化)
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0; // 小时偏移量
// 处理分钟滚动到边界的情况
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
// 从59分滚动到0分小时加1
offset += 1;
} else if (oldVal == minValue && newVal == maxValue) {
// 从0分滚动到59分小时减1
offset -= 1;
}
// 如果需要调整小时
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset); // 调整小时
mHourSpinner.setValue(getCurrentHour()); // 更新小时选择器
updateDateControl(); // 更新日期控件
// 检查并更新AM/PM状态
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
@ -177,110 +139,78 @@ public class DateTimePicker extends FrameLayout {
updateAmPmControl();
}
}
mDate.set(Calendar.MINUTE, newVal); // 设置新的分钟值
onDateTimeChanged(); // 触发日期时间变化回调
mDate.set(Calendar.MINUTE, newVal);
onDateTimeChanged();
}
};
// AM/PM变化监听器
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
mIsAm = !mIsAm; // 切换AM/PM状态
// 根据AM/PM状态调整小时
mIsAm = !mIsAm;
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY); // 从PM切换到AM减12小时
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY); // 从AM切换到PM加12小时
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
updateAmPmControl(); // 更新AM/PM控件
onDateTimeChanged(); // 触发日期时间变化回调
updateAmPmControl();
onDateTimeChanged();
}
};
/**
*
*/
public interface OnDateTimeChangedListener {
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
int dayOfMonth, int hourOfDay, int minute);
}
/**
* 1使
* @param context
*/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
/**
* 2使24
* @param context
* @param date
*/
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
/**
* 3使24
* @param context
* @param date
* @param is24HourView 24
*/
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance(); // 初始化日历对象
mInitialising = true; // 标记为初始化阶段
// 根据当前小时确定AM/PM状态
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); // 设置长按更新间隔为100毫秒
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化AM/PM选择器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings(); // 获取AM/PM本地化字符串
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.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 更新控件到初始状态
// update controls to initial state
updateDateControl();
updateHourControl();
updateAmPmControl();
// 设置24小时制视图
set24HourView(is24HourView);
// 设置为当前时间
// set to current time
setCurrentDate(date);
// 设置启用状态
setEnabled(isEnabled());
// 初始化完成
// set the content descriptions
mInitialising = false;
}
@ -290,7 +220,6 @@ public class DateTimePicker extends FrameLayout {
return;
}
super.setEnabled(enabled);
// 设置所有子控件的启用状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
@ -304,16 +233,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @return
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
*
* @param date
* Set the current date
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
@ -323,15 +254,16 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @param year
* @param month
* @param dayOfMonth
* @param hourOfDay 24
* @param 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) {
int dayOfMonth, int hourOfDay, int minute) {
setCurrentYear(year);
setCurrentMonth(month);
setCurrentDay(dayOfMonth);
@ -340,237 +272,212 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @return
* Get current year
*
* @return The current year
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
*
* @param year
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
return; // 如果不是初始化且值未变化,直接返回
return;
}
mDate.set(Calendar.YEAR, year);
updateDateControl(); // 更新日期控件
onDateTimeChanged(); // 触发日期时间变化回调
updateDateControl();
onDateTimeChanged();
}
/**
*
* @return 0-11
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
*
* @param month 0-11
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
return; // 如果不是初始化且值未变化,直接返回
return;
}
mDate.set(Calendar.MONTH, month);
updateDateControl(); // 更新日期控件
onDateTimeChanged(); // 触发日期时间变化回调
updateDateControl();
onDateTimeChanged();
}
/**
*
* @return
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
*
* @param dayOfMonth
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
return; // 如果不是初始化且值未变化,直接返回
return;
}
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
updateDateControl(); // 更新日期控件
onDateTimeChanged(); // 触发日期时间变化回调
updateDateControl();
onDateTimeChanged();
}
/**
* 24
* @return 0-23
* 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);
}
/**
* 1224
* @return 121-12240-23
*/
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY; // 下午13-23转换为1-11
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour; // 0点转换为12点1-11点不变
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* 24
* @param hourOfDay 0-23
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return; // 如果不是初始化且值未变化,直接返回
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
// 12小时制下的特殊处理
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false; // 下午
mIsAm = false;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY; // 13-23转换为1-11
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true; // 上午
mIsAm = true;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY; // 0点转换为12点
hourOfDay = HOURS_IN_HALF_DAY;
}
}
updateAmPmControl(); // 更新AM/PM控件
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay); // 设置小时选择器值
onDateTimeChanged(); // 触发日期时间变化回调
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
*
* @return
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
*
* @param minute
* Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
return; // 如果不是初始化且值未变化,直接返回
return;
}
mMinuteSpinner.setValue(minute); // 设置分钟选择器值
mMinuteSpinner.setValue(minute);
mDate.set(Calendar.MINUTE, minute);
onDateTimeChanged(); // 触发日期时间变化回调
onDateTimeChanged();
}
/**
* 24
* @return true24false12
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
return mIs24HourView;
}
/**
* 24AM/PM
* @param is24HourView true24falseAM/PM
* 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; // 如果模式未变化,直接返回
return;
}
mIs24HourView = is24HourView;
// 设置AM/PM选择器的可见性
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
updateHourControl(); // 更新小时选择器范围
setCurrentHour(hour); // 重新设置小时(考虑模式转换)
updateAmPmControl(); // 更新AM/PM控件
updateHourControl();
setCurrentHour(hour);
updateAmPmControl();
}
/**
*
*
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
// 向前调整3天以便以当前日期为中心显示一周
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null); // 清除旧值
// 生成一周的日期显示值
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
// 格式化为"MM.dd EEEE"(月.日 星期)
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
mDateSpinner.setDisplayedValues(mDateDisplayValues); // 设置显示值
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2); // 设置当前选中位置为中间(当前日期)
mDateSpinner.invalidate(); // 刷新控件
mDateSpinner.setDisplayedValues(mDateDisplayValues);
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate();
}
/**
* AM/PM
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE); // 24小时制下隐藏AM/PM选择器
mAmPmSpinner.setVisibility(View.GONE);
} else {
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index); // 设置AM/PM值
mAmPmSpinner.setVisibility(View.VISIBLE); // 显示AM/PM选择器
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
/**
*
*/
private void updateHourControl() {
if (mIs24HourView) {
// 24小时制范围0-23
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
// 12小时制范围1-12
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
/**
*
* @param callback null
* 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());
}

@ -29,113 +29,62 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
*
* DateTimePicker
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
private Calendar mDate = Calendar.getInstance(); // 存储当前选择的日期时间
private boolean mIs24HourView; // 是否为24小时制
private OnDateTimeSetListener mOnDateTimeSetListener; // 日期时间设置监听器
private DateTimePicker mDateTimePicker; // 日期时间选择器控件
/**
*
*
*/
private Calendar mDate = Calendar.getInstance();
private boolean mIs24HourView;
private OnDateTimeSetListener mOnDateTimeSetListener;
private DateTimePicker mDateTimePicker;
public interface OnDateTimeSetListener {
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 创建日期时间选择器控件
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker); // 将控件添加到对话框
// 设置日期时间变化监听器
setView(mDateTimePicker);
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 更新内部Calendar对象的各个时间分量
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());
}
});
// 设置初始日期时间
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0); // 将秒设为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小时制
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);
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 更新对话框标题
updateTitle(mDate.getTimeInMillis());
}
/**
* 24
* @param is24HourView true24false12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
* @param date
*/
private void updateTitle(long date) {
// 设置日期时间显示格式标志
int flag =
DateUtils.FORMAT_SHOW_YEAR | // 显示年份
DateUtils.FORMAT_SHOW_DATE | // 显示日期
DateUtils.FORMAT_SHOW_TIME; // 显示时间
// 根据是否24小时制添加相应的标志
// 注意这里有代码问题无论mIs24HourView为何值都使用FORMAT_24HOUR
// 应该是mIs24HourView ? DateUtils.FORMAT_24HOUR : 0
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
// 使用DateUtils格式化日期时间为字符串并设置为对话框标题
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
*
* @param arg0
* @param arg1
*/
public void onClick(DialogInterface arg0, int arg1) {
// 当用户点击确定按钮时,回调日期时间设置监听器
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}
}
}

@ -27,34 +27,17 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
/**
*
* ButtonPopupMenu
*/
public class DropdownMenu {
private Button mButton; // 触发下拉菜单的按钮
private PopupMenu mPopupMenu; // 弹出式菜单
private Menu mMenu; // 菜单项容器
private Button mButton;
private PopupMenu mPopupMenu;
private Menu mMenu;
/**
*
* @param context
* @param button
* @param menuId IDR.menu.xxx
*/
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button;
// 设置按钮背景为下拉箭头图标
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建PopupMenu以按钮为锚点
mPopupMenu = new PopupMenu(context, mButton);
// 获取PopupMenu的Menu对象
mMenu = mPopupMenu.getMenu();
// 从资源文件加载菜单项
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 设置按钮点击监听器,点击时显示下拉菜单
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mPopupMenu.show();
@ -62,29 +45,16 @@ public class DropdownMenu {
});
}
/**
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
* ID
* @param id ID
* @return MenuItem
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
/**
*
* @param title
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}

@ -28,109 +28,53 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* CursorAdapter
*/
public class FoldersListAdapter extends CursorAdapter {
// 数据库查询字段投影只需要ID和名称
public static final String [] PROJECTION = {
NoteColumns.ID, // 文件夹ID
NoteColumns.SNIPPET // 文件夹名称存储在SNIPPET字段中
NoteColumns.ID,
NoteColumns.SNIPPET
};
// 列索引常量定义
public static final int ID_COLUMN = 0; // ID列索引
public static final int NAME_COLUMN = 1; // 名称列索引
public static final int ID_COLUMN = 0;
public static final int NAME_COLUMN = 1;
/**
*
* @param context
* @param c Cursor
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO: 自动生成的构造函数存根
// TODO Auto-generated constructor stub
}
/**
*
* @param context
* @param cursor Cursor
* @param parent
* @return FolderListItem
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
/**
*
* @param view
* @param context
* @param cursor Cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 检查视图类型是否为FolderListItem
if (view instanceof FolderListItem) {
String folderName;
// 特殊处理根文件夹如果ID是根文件夹ID则显示特定的父文件夹名称
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
folderName = context.getString(R.string.menu_move_parent_folder);
} else {
folderName = cursor.getString(NAME_COLUMN);
}
// 调用FolderListItem的bind方法设置文件夹名称
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
/**
*
* @param context
* @param position
* @return
*/
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position); // 获取对应位置的Cursor数据
// 特殊处理根文件夹如果ID是根文件夹ID则返回特定的父文件夹名称
if (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) {
return context.getString(R.string.menu_move_parent_folder);
} else {
return cursor.getString(NAME_COLUMN);
}
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);
}
/**
*
* LinearLayout
*/
private class FolderListItem extends LinearLayout {
private TextView mName; // 显示文件夹名称的TextView
private TextView mName;
/**
*
* @param context
*/
public FolderListItem(Context context) {
super(context);
// 加载布局文件
inflate(context, R.layout.folder_list_item, this);
// 初始化TextView
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
*
* @param name
*/
public void bind(String name) {
mName.setText(name);
}
}
}

@ -1,74 +0,0 @@
package net.micode.notes.ui;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import net.micode.notes.R;
/**
*
*/
public class ImageInsertDialogFragment extends DialogFragment {
public static final String TAG = "ImageInsertDialogFragment";
public interface OnImageInsertListener {
void onGallerySelected();
void onCameraSelected();
}
private OnImageInsertListener mListener;
public ImageInsertDialogFragment() {
// Required empty public constructor
}
public void setOnImageInsertListener(OnImageInsertListener listener) {
mListener = listener;
}
@Override
public int getTheme() {
return R.style.NoteTheme;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_image_insert, container, false);
View btnGallery = view.findViewById(R.id.btn_gallery);
View btnCamera = view.findViewById(R.id.btn_camera);
View btnCancel = view.findViewById(R.id.btn_cancel);
btnGallery.setOnClickListener(v -> {
if (mListener != null) {
mListener.onGallerySelected();
}
dismiss();
});
btnCamera.setOnClickListener(v -> {
if (mListener != null) {
mListener.onCameraSelected();
}
dismiss();
});
btnCancel.setOnClickListener(v -> dismiss());
return view;
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
mListener = null;
}
}

@ -1,112 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.account.AccountManager;
import com.google.android.material.appbar.MaterialToolbar;
public class LoginActivity extends Activity {
private EditText etUsername;
private EditText etPassword;
private Button btnLogin;
private Button btnCancel;
private TextView tvRegister;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
}
}
initViews();
setupListeners();
}
private void initViews() {
etUsername = (EditText) findViewById(R.id.et_login_username);
etPassword = (EditText) findViewById(R.id.et_login_password);
btnLogin = (Button) findViewById(R.id.btn_login);
btnCancel = (Button) findViewById(R.id.btn_login_cancel);
tvRegister = (TextView) findViewById(R.id.tv_login_register);
}
private void setupListeners() {
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleLogin();
}
});
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
tvRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(LoginActivity.this, RegisterActivity.class);
startActivity(intent);
}
});
}
private void handleLogin() {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
if (TextUtils.isEmpty(username)) {
Toast.makeText(this, R.string.error_username_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(password)) {
Toast.makeText(this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (AccountManager.login(this, username, password)) {
Toast.makeText(this, R.string.toast_login_success, Toast.LENGTH_SHORT).show();
finish();
} else {
Toast.makeText(this, R.string.error_login_failed, Toast.LENGTH_SHORT).show();
}
}
}

File diff suppressed because it is too large Load Diff

@ -37,125 +37,83 @@ import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
/**
* EditText
*
*/
public class NoteEditText extends EditText {
private static final String TAG = "NoteEditText"; // 日志标签
private int mIndex; // 当前EditText在列表中的索引位置
private int mSelectionStartBeforeDelete; // 删除操作前的光标起始位置
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 String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
// 链接协议与对应字符串资源的映射表
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
// 初始化链接协议与字符串资源的映射
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
* NoteEditActivity
*
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
*
* @param index
* @param text
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
void onEditTextDelete(int index, String text);
/**
*
* @param index
* @param text
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
void onEditTextEnter(int index, String text);
/**
*
* @param index
* @param hasText
* Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener; // 文本变化监听器实例
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
* 1
* @param context
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 初始化索引为0
mIndex = 0;
}
/**
*
* @param index
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
* @param listener
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* 2
* @param context
* @param attrs
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* 3
* @param context
* @param attrs
* @param defStyle
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 按下事件
// 计算点击位置在文本布局中的偏移量
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft(); // 减去内边距
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX(); // 加上滚动偏移
x += getScrollX();
y += getScrollY();
// 获取文本布局并计算点击位置的字符偏移
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
// 设置光标位置
Selection.setSelection(getText(), off);
break;
}
@ -163,22 +121,15 @@ public class NoteEditText extends EditText {
return super.onTouchEvent(event);
}
/**
*
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER: // 回车键
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
return false; // 返回false让系统继续处理回车事件
return false;
}
break;
case KeyEvent.KEYCODE_DEL: // 删除键
// 记录删除前的光标起始位置
case KeyEvent.KEYCODE_DEL:
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
@ -187,37 +138,27 @@ public class NoteEditText extends EditText {
return super.onKeyDown(keyCode, event);
}
/**
*
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL: // 删除键释放
case KeyEvent.KEYCODE_DEL:
if (mOnTextViewChangeListener != null) {
// 如果光标在开头且不是第一个编辑框,则删除当前编辑框
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true; // 消耗事件,不再继续处理
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted"); // 监听器未设置日志
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER: // 回车键释放
case KeyEvent.KEYCODE_ENTER:
if (mOnTextViewChangeListener != null) {
// 获取光标位置及之后的文本
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
// 截断光标后的文本
setText(getText().subSequence(0, selectionStart));
// 通知Activity在当前编辑框后添加新编辑框
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted"); // 监听器未设置日志
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
@ -226,46 +167,30 @@ public class NoteEditText extends EditText {
return super.onKeyUp(keyCode, event);
}
/**
*
* @param focused
* @param direction
* @param previouslyFocusedRect
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
// 根据是否有文本内容通知监听器更新UI状态
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false); // 无文本
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true); // 有文本
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
*
* @param menu
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
// 检查文本是否包含富文本(如链接)
if (getText() instanceof Spanned) {
int selStart = getSelectionStart(); // 选择起始位置
int selEnd = getSelectionEnd(); // 选择结束位置
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
// 计算选择范围
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
// 获取选择范围内的URLSpan对象
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
// 如果只选择了一个链接
if (urls.length == 1) {
int defaultResId = 0; // 默认资源ID
// 遍历协议映射表,匹配链接类型
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
@ -273,16 +198,14 @@ public class NoteEditText extends EditText {
}
}
// 如果没有匹配到已知协议,使用"其他链接"选项
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 添加菜单项并设置点击监听器
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 触发链接点击事件(跳转到相应应用)
// goto a new intent
urls[0].onClick(NoteEditText.this);
return true;
}

@ -18,7 +18,6 @@ package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.text.Html;
import android.text.TextUtils;
import net.micode.notes.data.Contact;
@ -26,30 +25,23 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
/**
*
*
* 访
*/
public class NoteItemData {
// 数据库查询字段投影,定义需要从数据库中获取的列
static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记ID
NoteColumns.ALERTED_DATE, // 提醒日期
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建日期
NoteColumns.HAS_ATTACHMENT, // 是否有附件
NoteColumns.MODIFIED_DATE, // 修改日期
NoteColumns.NOTES_COUNT, // 笔记数量(对于文件夹类型)
NoteColumns.PARENT_ID, // 父文件夹ID
NoteColumns.SNIPPET, // 内容摘要
NoteColumns.TYPE, // 类型(笔记、文件夹、系统文件夹等)
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
NoteColumns.IS_LOCKED, // 是否加锁
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,
};
// 字段索引常量定义对应PROJECTION数组中的位置
private static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
@ -62,41 +54,29 @@ public class NoteItemData {
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
private static final int IS_LOCKED_COLUMN = 12;
// 笔记数据字段
private long mId; // 笔记ID
private long mAlertDate; // 提醒时间
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; // 笔记类型
private int mWidgetId; // 关联的小部件ID
private int mWidgetType; // 小部件类型
private boolean mIsLocked; // 是否加锁
// 通话记录相关字段
private String mName; // 联系人姓名
private String mPhoneNumber; // 电话号码
// 列表位置状态标识
private boolean mIsLastItem; // 是否为最后一项
private boolean mIsFirstItem; // 是否为第一项
private boolean mIsOnlyOneItem; // 是否只有一项
private boolean mIsOneNoteFollowingFolder; // 是否是单个笔记跟随文件夹后
private boolean mIsMultiNotesFollowingFolder; // 是否是多个笔记跟随文件夹后
/**
*
* @param context
* @param cursor
*/
private long mId;
private long mAlertDate;
private int mBgColorId;
private long mCreatedDate;
private boolean mHasAttachment;
private long mModifiedDate;
private int mNotesCount;
private long mParentId;
private String mSnippet;
private int mType;
private int mWidgetId;
private int mWidgetType;
private String mName;
private String mPhoneNumber;
private boolean mIsLastItem;
private boolean mIsFirstItem;
private boolean mIsOnlyOneItem;
private boolean mIsOneNoteFollowingFolder;
private boolean mIsMultiNotesFollowingFolder;
public NoteItemData(Context context, Cursor cursor) {
// 从游标中读取各字段值
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
@ -106,73 +86,47 @@ public class NoteItemData {
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
mSnippet = cursor.getString(SNIPPET_COLUMN);
// 将HTML格式转换为普通文本解决富文本编辑后显示乱码的问题
if (!TextUtils.isEmpty(mSnippet)) {
mSnippet = Html.fromHtml(mSnippet).toString();
}
// 移除摘要中的复选框标记(已勾选和未勾选)
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0);
// 初始化通话记录相关字段
mPhoneNumber = "";
// 如果是通话记录文件夹中的笔记
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;
}
}
}
// 确保名称不为null
if (mName == null) {
mName = "";
}
// 检查当前项在列表中的位置状态
checkPostion(cursor);
}
/**
*
* @param cursor
*/
private void checkPostion(Cursor cursor) {
// 设置基础位置标识
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
mIsOnlyOneItem = (cursor.getCount() == 1);
// 初始化文件夹相关标识
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 如果是笔记类型且不是第一项,检查前一项是否为文件夹
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
int position = cursor.getPosition();
// 移动到前一项
if (cursor.moveToPrevious()) {
// 检查前一项是否是文件夹或系统文件夹
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
// 判断是单个笔记还是多个笔记跟随文件夹
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true; // 多个笔记跟随文件夹
mIsMultiNotesFollowingFolder = true;
} else {
mIsOneNoteFollowingFolder = true; // 单个笔记跟随文件夹
mIsOneNoteFollowingFolder = true;
}
}
// 移回当前位置
if (!cursor.moveToNext()) {
throw new IllegalStateException("cursor move to previous but can't move back");
}
@ -180,187 +134,90 @@ public class NoteItemData {
}
}
/**
*
* @return true
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
*
* @return true
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
* @return true
*/
public boolean isLast() {
return mIsLastItem;
}
/**
*
* @return
*/
public String getCallName() {
return mName;
}
/**
*
* @return true
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
* @return true
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
/**
* ID
* @return ID
*/
public long getId() {
return mId;
}
/**
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
* @return
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
*
* @return true
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
* @return
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* IDgetParentId
* @return ID
*/
public long getFolderId () {
return mParentId;
}
/**
*
* @return
*/
public int getType() {
return mType;
}
/**
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* ID
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
* @return true
*/
public boolean hasAlert() {
return (mAlertDate > 0);
}
/**
*
* @return true
*/
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
* 便
* @return 便true
*/
public boolean isLocked() {
return mIsLocked;
}
/**
*
* @param cursor
* @return
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}

File diff suppressed because it is too large Load Diff

@ -30,94 +30,58 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
*
* ListView
*
*/
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; // 是否处于多选模式
/**
*
*
*/
private static final String TAG = "NotesListAdapter";
private Context mContext;
private HashMap<Integer, Boolean> mSelectedIndex;
private int mNotesCount;
private boolean mChoiceMode;
public static class AppWidgetAttribute {
public int widgetId; // 小部件ID
public int widgetType; // 小部件类型
public int widgetId;
public int widgetType;
};
/**
*
* @param context
*/
public NotesListAdapter(Context context) {
super(context, null); // 初始游标为null
mSelectedIndex = new HashMap<Integer, Boolean>(); // 初始化选中索引映射表
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0; // 初始化笔记数量为0
mNotesCount = 0;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 创建新的列表项视图
return new NotesListItem(context);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 绑定数据到视图
if (view instanceof NotesListItem) {
// 从游标创建笔记数据项
NoteItemData itemData = new NoteItemData(context, cursor);
// 绑定数据到列表项,传递是否处于多选模式和是否选中状态
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
/**
*
* @param position
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked); // 更新选中状态映射
notifyDataSetChanged(); // 通知数据变化,刷新视图
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
/**
*
* @return true
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
*
* @param mode truefalse退
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear(); // 清空选中状态
mChoiceMode = mode; // 设置多选模式标志
mSelectedIndex.clear();
mChoiceMode = mode;
}
/**
*
*
* @param checked truefalse
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor(); // 获取当前游标
// 遍历所有项
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
// 只处理笔记类型,不处理文件夹类型
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
@ -125,21 +89,15 @@ public class NotesListAdapter extends CursorAdapter {
}
}
/**
* 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
// 排除根文件夹ID不应该被选中
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id); // 添加到集合
itemSet.add(id);
}
}
}
@ -147,46 +105,36 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
/**
*
* @return null
*/
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); // 获取对应位置的游标
Cursor c = (Cursor) getItem(position);
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId(); // 设置小部件ID
widget.widgetType = item.getWidgetType(); // 设置小部件类型
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
/**
*
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null; // 游标无效时返回null
return null;
}
}
}
return itemSet;
}
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values(); // 获取所有选中状态值
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
// 统计选中状态为true的数量
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
@ -195,23 +143,14 @@ public class NotesListAdapter extends CursorAdapter {
return count;
}
/**
*
* @return 0true
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount(); // 获取选中数量
return (checkedCount != 0 && checkedCount == mNotesCount); // 比较选中数量和笔记总数
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
* @param position
* @return truefalse
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false; // 映射表中不存在该位置,默认为未选中
return false;
}
return mSelectedIndex.get(position);
}
@ -219,32 +158,26 @@ public class NotesListAdapter extends CursorAdapter {
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount(); // 内容变化时重新计算笔记数量
calcNotesCount();
}
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount(); // 游标变化时重新计算笔记数量
calcNotesCount();
}
/**
*
*
*/
private void calcNotesCount() {
mNotesCount = 0; // 重置计数
// 遍历所有项
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i); // 获取对应位置的游标
Cursor c = (Cursor) getItem(i);
if (c != null) {
// 只统计笔记类型
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return; // 游标无效时提前返回
return;
}
}
}

@ -27,47 +27,28 @@ import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ElderModeUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
/**
*
* UI
*/
public class NotesListItem extends LinearLayout {
private ImageView mAlert; // 提醒图标
private ImageView mLockIcon; // 锁图标
private TextView mTitle; // 标题/内容摘要
private TextView mTime; // 修改时间
private TextView mCallName; // 通话记录联系人姓名
private NoteItemData mItemData; // 绑定的笔记数据
private CheckBox mCheckBox; // 多选模式下的复选框
private ImageView mAlert;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
private NoteItemData mItemData;
private CheckBox mCheckBox;
/**
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this); // 从布局文件初始化视图
// 初始化视图组件
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mLockIcon = (ImageView) findViewById(R.id.iv_lock_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
/**
*
* @param context
* @param data
* @param choiceMode
* @param checked
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 多选模式处理:如果是笔记类型且处于多选模式,显示复选框
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@ -75,46 +56,19 @@ public class NotesListItem extends LinearLayout {
mCheckBox.setVisibility(View.GONE);
}
mItemData = data; // 保存数据引用
// 根据加锁状态显示/隐藏锁图标
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isLocked()) {
mLockIcon.setVisibility(View.VISIBLE);
} else {
mLockIcon.setVisibility(View.GONE);
}
} else {
mLockIcon.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.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.getId() == Notes.ID_TRASH_FOLDER) {
// 回收站文件夹的特殊显示
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置主标题样式
// 显示回收站名称和包含的笔记数量
mTitle.setText("回收站"
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
// 设置回收站图标(使用系统默认图标)
mAlert.setImageResource(android.R.drawable.ic_delete);
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())); // 显示格式化后的内容摘要
// 根据是否有提醒设置提醒图标
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);
@ -122,20 +76,16 @@ public class NotesListItem extends LinearLayout {
mAlert.setVisibility(View.GONE);
}
} else {
// 普通笔记或文件夹的显示
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem); // 设置主标题样式
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()));
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);
@ -144,61 +94,28 @@ public class NotesListItem extends LinearLayout {
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 显示相对时间(如"2小时前"- 回收站中的笔记和回收站文件夹本身都不显示时间
if (data.getParentId() != Notes.ID_TRASH_FOLDER && data.getId() != Notes.ID_TRASH_FOLDER) {
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
mTime.setVisibility(View.VISIBLE);
} else {
mTime.setVisibility(View.GONE);
}
// 根据数据类型和位置设置背景
setBackground(data);
// 在设置完所有内容后应用老年人模式,确保字体大小正确
ElderModeUtils.applyElderMode(context, mTitle);
}
/**
*
* @param data
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId(); // 获取背景颜色ID
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
// 普通笔记类型的背景设置
if (data.isSingle() || data.isOneFollowingFolder()) {
// 单一条目或紧跟在文件夹后的单个笔记:使用单个笔记背景
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
// 最后一项:使用最后一项背景
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
// 第一项或紧跟在文件夹后的多个笔记:使用第一项背景
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
// 中间项:使用普通背景
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
// 文件夹类型:使用文件夹背景
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
// 回收站中的笔记使用半透明效果,与普通笔记区分
if (data.getType() == Notes.TYPE_NOTE && data.getParentId() == Notes.ID_TRASH_FOLDER) {
setAlpha(0.6f); // 设置透明度为60%
} else {
setAlpha(1.0f); // 普通笔记使用完全不透明
}
}
/**
*
* @return
*/
public NoteItemData getItemData() {
return mItemData;
}

@ -2,7 +2,7 @@
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may use this file except in compliance with 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
@ -16,8 +16,12 @@
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
@ -27,48 +31,59 @@ 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.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.preference.PreferenceCategory;
import net.micode.notes.R;
import net.micode.notes.security.PasswordManager;
import net.micode.notes.account.AccountManager;
import com.google.android.material.appbar.MaterialToolbar;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
public class NotesPreferenceActivity extends PreferenceActivity {
private static final String PREFERENCE_SECURITY_KEY = "pref_security_settings";
private static final String PREFERENCE_USER_CENTER_KEY = "pref_user_center";
public static final String PREFERENCE_ELDER_MODE_KEY = "pref_key_elder_mode";
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";
private static final String PREFERENCE_NAME = "notes_preferences";
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
private static final String AUTHORITIES_FILTER_KEY = "authorities";
private PreferenceCategory mAccountCategory;
private boolean mHasCheckedSecurityQuestions = false;
private GTaskReceiver mReceiver;
private Account[] mOriAccounts;
private boolean mHasAddedAccount;
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
// 从XML文件加载偏好设置
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
// 设置ActionBar
android.app.ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
}
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
@ -76,578 +91,289 @@ public class NotesPreferenceActivity extends PreferenceActivity {
@Override
protected void onResume() {
super.onResume();
// 检查并设置密保问题(只在首次设置时显示,且只检查一次)
if (!mHasCheckedSecurityQuestions && !hasSecurityQuestionsSet()) {
mHasCheckedSecurityQuestions = true;
showSetSecurityQuestionsDialog();
}
loadSecurityPreference();
loadUserCenterPreference();
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
if (PasswordManager.isPasswordSet(this)) {
securityPref.setSummary(R.string.preferences_password_set);
} else {
securityPref.setSummary(R.string.preferences_password_not_set);
}
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
showSecuritySettingsDialog();
return true;
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
});
}
}
}
private void loadUserCenterPreference() {
Preference userCenterPref = findPreference(PREFERENCE_USER_CENTER_KEY);
if (userCenterPref != null) {
if (AccountManager.isUserLoggedIn(this)) {
String currentUser = AccountManager.getCurrentUser(this);
userCenterPref.setSummary(getString(R.string.preferences_user_center_summary, currentUser));
} else {
userCenterPref.setSummary(getString(R.string.preferences_user_center_not_logged_in));
}
refreshUI();
}
userCenterPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
showUserCenterDialog();
return true;
}
});
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
* @return
*/
private boolean hasSecurityQuestionsSet() {
SharedPreferences preferences = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE);
String name = preferences.getString("security_question_name", "");
String birthday = preferences.getString("security_question_birthday", "");
return !name.isEmpty() && !birthday.isEmpty();
}
/**
*
*/
private void showSetSecurityQuestionsDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("设置密保问题");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setMessage("请设置您的个人信息作为密保,用于重置密码时验证身份");
// 创建布局
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 20, 40, 20);
// 创建姓名输入框
final EditText nameInput = new EditText(this);
nameInput.setHint("请输入姓名");
layout.addView(nameInput);
// 创建生日输入框
final EditText birthdayInput = new EditText(this);
birthdayInput.setHint("请输入生日 (YYYY-MM-DD)");
layout.addView(birthdayInput);
builder.setView(layout);
// 设置确定按钮
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String name = nameInput.getText().toString();
String birthday = birthdayInput.getText().toString();
if (!name.isEmpty() && !birthday.isEmpty()) {
// 保存密保信息
SharedPreferences preferences = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("security_question_name", name);
editor.putString("security_question_birthday", birthday);
editor.apply();
// 显示提示
Toast.makeText(NotesPreferenceActivity.this, "密保设置成功", Toast.LENGTH_SHORT).show();
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
// 信息为空,显示提示
Toast.makeText(NotesPreferenceActivity.this, "请完整填写个人信息", Toast.LENGTH_SHORT).show();
// 重新显示对话框
showSetSecurityQuestionsDialog();
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
});
// 设置取消按钮
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 如果用户取消,退出设置界面
finish();
}
});
// 设置对话框不可取消
builder.setCancelable(false);
// 显示对话框
builder.show();
mAccountCategory.addPreference(accountPref);
}
/**
*
*/
private void showVerifySecurityQuestionsDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("验证身份");
builder.setIcon(android.R.drawable.ic_lock_idle_lock);
builder.setMessage("请回答您的密保问题以验证身份");
// 创建布局
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 20, 40, 20);
// 创建姓名输入框
final EditText nameInput = new EditText(this);
nameInput.setHint("请输入姓名");
layout.addView(nameInput);
// 创建生日输入框
final EditText birthdayInput = new EditText(this);
birthdayInput.setHint("请输入生日 (YYYY-MM-DD)");
layout.addView(birthdayInput);
builder.setView(layout);
// 设置确定按钮
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String name = nameInput.getText().toString();
String birthday = birthdayInput.getText().toString();
if (verifySecurityQuestions(name, birthday)) {
// 验证成功,允许进入设置
Toast.makeText(NotesPreferenceActivity.this, "验证成功", Toast.LENGTH_SHORT).show();
} else {
// 验证失败,显示提示
Toast.makeText(NotesPreferenceActivity.this, "验证失败,请重新输入", Toast.LENGTH_SHORT).show();
// 重新显示验证对话框
showVerifySecurityQuestionsDialog();
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// set button state
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// set last sync time
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
});
// 设置取消按钮
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 如果用户取消,退出设置界面
finish();
}
});
// 设置对话框不可取消
builder.setCancelable(false);
// 显示对话框
builder.show();
}
/**
*
* @param name
* @param birthday
* @return
*/
private boolean verifySecurityQuestions(String name, String birthday) {
SharedPreferences preferences = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE);
String savedName = preferences.getString("security_question_name", "");
String savedBirthday = preferences.getString("security_question_birthday", "");
return savedName.equals(name) && savedBirthday.equals(birthday);
}
/**
*
* @param context
* @param name
* @param birthday
* @return
*/
public static boolean verifySecurityQuestions(Context context, String name, String birthday) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
String savedName = preferences.getString("security_question_name", "");
String savedBirthday = preferences.getString("security_question_birthday", "");
return savedName.equals(name) && savedBirthday.equals(birthday);
}
}
/**
*
* @param context
* @return
*/
public static boolean hasSecurityQuestionsSet(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
String name = preferences.getString("security_question_name", "");
String birthday = preferences.getString("security_question_birthday", "");
return !name.isEmpty() && !birthday.isEmpty();
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
private void showUserCenterDialog() {
final boolean isLoggedIn = AccountManager.isUserLoggedIn(this);
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
String[] items;
if (isLoggedIn) {
items = new String[] {
getString(R.string.menu_logout)
};
} else {
items = new String[] {
getString(R.string.menu_login),
getString(R.string.menu_register)
};
}
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));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.preferences_user_center_title));
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (isLoggedIn) {
if (which == 0) {
handleLogout();
}
} else {
if (which == 0) {
Intent intent = new Intent(NotesPreferenceActivity.this, LoginActivity.class);
startActivity(intent);
} else if (which == 1) {
Intent intent = new Intent(NotesPreferenceActivity.this, RegisterActivity.class);
startActivity(intent);
}
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
});
builder.show();
// 添加修改密保问题的选项
if (mAccountCategory != null) {
Preference securityPref = new Preference(this);
securityPref.setTitle("修改密保问题");
securityPref.setSummary("修改用于重置密码的个人信息");
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// 验证当前密保才能修改
showVerifySecurityQuestionsForModificationDialog();
return true;
}
});
mAccountCategory.addPreference(securityPref); // 添加密保设置选项
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
}
/**
*
*/
private void showChangeSecurityQuestionsDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("修改密保问题");
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setMessage("请重新设置您的个人信息作为密保");
// 创建布局
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 20, 40, 20);
// 创建姓名输入框
final EditText nameInput = new EditText(this);
nameInput.setHint("请输入姓名");
layout.addView(nameInput);
// 创建生日输入框
final EditText birthdayInput = new EditText(this);
birthdayInput.setHint("请输入生日 (YYYY-MM-DD)");
layout.addView(birthdayInput);
builder.setView(layout);
// 设置确定按钮
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String name = nameInput.getText().toString();
String birthday = birthdayInput.getText().toString();
if (!name.isEmpty() && !birthday.isEmpty()) {
// 保存新的密保信息
SharedPreferences preferences = getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("security_question_name", name);
editor.putString("security_question_birthday", birthday);
editor.apply();
// 显示提示
Toast.makeText(NotesPreferenceActivity.this, "密保修改成功", Toast.LENGTH_SHORT).show();
} else {
// 信息为空,显示提示
Toast.makeText(NotesPreferenceActivity.this, "请完整填写个人信息", Toast.LENGTH_SHORT).show();
}
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();
}
});
// 设置取消按钮
builder.setNegativeButton("取消", null);
// 显示对话框
builder.show();
}
/**
*
*/
private void showVerifySecurityQuestionsForModificationDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("验证身份");
builder.setIcon(android.R.drawable.ic_lock_idle_lock);
builder.setMessage("请回答您的密保问题以验证身份,验证成功后才能修改密保");
// 创建布局
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(40, 20, 40, 20);
// 创建姓名输入框
final EditText nameInput = new EditText(this);
nameInput.setHint("请输入姓名");
layout.addView(nameInput);
// 创建生日输入框
final EditText birthdayInput = new EditText(this);
birthdayInput.setHint("请输入生日 (YYYY-MM-DD)");
layout.addView(birthdayInput);
builder.setView(layout);
// 设置确定按钮
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
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) {
String name = nameInput.getText().toString();
String birthday = birthdayInput.getText().toString();
if (verifySecurityQuestions(name, birthday)) {
// 验证成功,显示修改密保对话框
Toast.makeText(NotesPreferenceActivity.this, "验证成功", Toast.LENGTH_SHORT).show();
showChangeSecurityQuestionsDialog();
} else {
// 验证失败,显示提示
Toast.makeText(NotesPreferenceActivity.this, "验证失败,请重新输入", Toast.LENGTH_SHORT).show();
// 重新显示验证对话框
showVerifySecurityQuestionsForModificationDialog();
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
// 设置取消按钮
builder.setNegativeButton("取消", null);
// 显示对话框
builder.show();
dialogBuilder.show();
}
private void handleLogout() {
if (AccountManager.logout(this)) {
Toast.makeText(this, R.string.toast_logout_success, Toast.LENGTH_SHORT).show();
loadUserCenterPreference();
} else {
Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
private void showSecuritySettingsDialog() {
final boolean passwordSet = PasswordManager.isPasswordSet(this);
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
String[] items;
if (passwordSet) {
items = new String[] {
getString(R.string.menu_change_password),
getString(R.string.menu_clear_password)
};
} else {
items = new String[] {
getString(R.string.menu_set_password)
};
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.preferences_security_title));
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (passwordSet) {
if (which == 0) {
showChangePasswordDialog();
} else if (which == 1) {
showClearPasswordConfirmDialog();
}
} else {
showSetPasswordDialog();
}
}
});
builder.show();
}
private void showSetPasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name);
etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
etPassword.setHint(R.string.hint_login_password);
etPassword.setText("");
builder.setTitle(R.string.title_set_password);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password = etPassword.getText().toString();
if (password.isEmpty()) {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (PasswordManager.setPassword(NotesPreferenceActivity.this, password)) {
Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_set_success, Toast.LENGTH_SHORT).show();
loadSecurityPreference();
} else {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, 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();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}).start();
}
private void showChangePasswordDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name);
etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
etPassword.setHint(R.string.hint_new_password);
etPassword.setText("");
builder.setTitle(R.string.title_change_password);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String newPassword = etPassword.getText().toString();
if (newPassword.isEmpty()) {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
showOldPasswordVerificationDialog(newPassword);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
private void showOldPasswordVerificationDialog(final String newPassword) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name);
etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
etPassword.setHint(R.string.hint_old_password);
etPassword.setText("");
builder.setTitle(R.string.title_verify_password);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String oldPassword = etPassword.getText().toString();
if (oldPassword.isEmpty()) {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (PasswordManager.verifyPassword(NotesPreferenceActivity.this, oldPassword)) {
if (PasswordManager.setPassword(NotesPreferenceActivity.this, newPassword)) {
Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_change_success, Toast.LENGTH_SHORT).show();
loadSecurityPreference();
} else {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
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();
}
private void showClearPasswordConfirmDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setMessage(getString(R.string.message_clear_password_confirm));
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
showPasswordDialogForClear();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
private void showPasswordDialogForClear() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = LayoutInflater.from(this).inflate(R.layout.dialog_edit_text, null);
final EditText etPassword = (EditText) view.findViewById(R.id.et_foler_name);
etPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
etPassword.setHint(R.string.hint_enter_password);
etPassword.setText("");
builder.setTitle(R.string.title_verify_password);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password = etPassword.getText().toString();
if (password.isEmpty()) {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (PasswordManager.verifyPassword(NotesPreferenceActivity.this, password)) {
if (PasswordManager.clearPassword(NotesPreferenceActivity.this)) {
Toast.makeText(NotesPreferenceActivity.this, R.string.toast_password_cleared, Toast.LENGTH_SHORT).show();
loadSecurityPreference();
} else {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(NotesPreferenceActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show();
}
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));
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:

@ -1,113 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.account.AccountManager;
import com.google.android.material.appbar.MaterialToolbar;
public class RegisterActivity extends Activity {
private EditText etUsername;
private EditText etPassword;
private EditText etConfirmPassword;
private Button btnRegister;
private Button btnCancel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
}
}
initViews();
setupListeners();
}
private void initViews() {
etUsername = (EditText) findViewById(R.id.et_register_username);
etPassword = (EditText) findViewById(R.id.et_register_password);
etConfirmPassword = (EditText) findViewById(R.id.et_register_confirm_password);
btnRegister = (Button) findViewById(R.id.btn_register);
btnCancel = (Button) findViewById(R.id.btn_register_cancel);
}
private void setupListeners() {
btnRegister.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleRegister();
}
});
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
private void handleRegister() {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
String confirmPassword = etConfirmPassword.getText().toString().trim();
if (TextUtils.isEmpty(username)) {
Toast.makeText(this, R.string.error_username_empty, Toast.LENGTH_SHORT).show();
return;
}
if (TextUtils.isEmpty(password)) {
Toast.makeText(this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (!password.equals(confirmPassword)) {
Toast.makeText(this, R.string.error_password_mismatch, Toast.LENGTH_SHORT).show();
return;
}
if (AccountManager.isUserExists(this, username)) {
Toast.makeText(this, R.string.error_username_exists, Toast.LENGTH_SHORT).show();
return;
}
if (AccountManager.register(this, username, password)) {
Toast.makeText(this, R.string.toast_register_success, Toast.LENGTH_SHORT).show();
finish();
} else {
Toast.makeText(this, R.string.error_register_failed, Toast.LENGTH_SHORT).show();
}
}
}

@ -15,7 +15,6 @@
*/
package net.micode.notes.widget;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
@ -33,150 +32,101 @@ import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
/**
*
*
* 2x4x
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
// 数据库查询字段投影
public static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 笔记ID
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.SNIPPET // 内容摘要
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
// 字段索引常量
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
private static final String TAG = "NoteWidgetProvider"; // 日志标签
private static final String TAG = "NoteWidgetProvider";
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 当小部件从桌面删除时调用
ContentValues values = new ContentValues();
// 将笔记的小部件ID重置为无效值解除笔记与小部件的关联
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.WIDGET_ID + "=?", // 更新条件找到对应的小部件ID
NoteColumns.WIDGET_ID + "=?",
new String[] { String.valueOf(appWidgetIds[i])});
}
}
/**
* ID
* @param context
* @param widgetId ID
* @return ID
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
// 查询条件小部件ID匹配且不在回收站中
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLDER) },
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
null);
}
/**
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
/**
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
boolean privacyMode) {
for (int i = 0; i < appWidgetIds.length; i++) {
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
int bgId = ResourceParser.getDefaultBgId(context); // 默认背景颜色ID
String snippet = ""; // 内容摘要
int bgId = ResourceParser.getDefaultBgId(context);
String snippet = "";
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 设置Activity启动模式
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 查询与小部件关联的笔记信息
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
// 正常情况下一个小部件ID应该只关联一个笔记
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return; // 发现异常情况,直接返回
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); // 查看笔记
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
intent.setAction(Intent.ACTION_VIEW);
} else {
// 没有找到关联的笔记,显示默认提示
snippet = context.getResources().getString(R.string.widget_havenot_content);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT); // 新建或编辑笔记
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
if (c != null) {
c.close();
}
// 创建RemoteViews对象
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); // 设置背景
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
* 宿ActivityPendingIntent
* Generate the pending intent to start host for the widget
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
// 隐私模式:显示占位文本,点击跳转到笔记列表
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 正常模式:显示笔记摘要,点击跳转到笔记编辑界面
rv.setTextViewText(R.id.widget_text, snippet);
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); // 设置点击事件
appWidgetManager.updateAppWidget(appWidgetIds[i], rv); // 更新小部件
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
/**
* IDID
* @param bgId ID
* @return ID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
* @return ID
*/
protected abstract int getLayoutId();
/**
*
* @return Notes.TYPE_WIDGET_2X, Notes.TYPE_WIDGET_4X
*/
protected abstract int getWidgetType();
}

@ -23,49 +23,25 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
/**
* 2x
* NoteWidgetProvider2x
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
/**
*
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds); // 调用父类的更新方法
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* 2xID
* @return 2xID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_2x; // 返回2x小部件的布局文件
return R.layout.widget_2x;
}
/**
* ID2xID
* @param bgId ID
* @return 2xID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); // 从资源解析器获取2x小部件背景资源
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
/**
*
* @return 2x
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X; // 返回2x小部件的类型标识
return Notes.TYPE_WIDGET_2X;
}
}

@ -23,49 +23,24 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
/**
* 4x
* NoteWidgetProvider4x
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
/**
*
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds); // 调用父类的更新方法
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* 4xID
* @return 4xID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_4x; // 返回4x小部件的布局文件
return R.layout.widget_4x;
}
/**
* ID4xID
* @param bgId ID
* @return 4xID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); // 从资源解析器获取4x小部件背景资源
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
/**
*
* @return 4x
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X; // 返回4x小部件的类型标识
return Notes.TYPE_WIDGET_4X;
}
}

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

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

Loading…
Cancel
Save