新增功能并完成合并 #8

Merged
pjs4euiwk merged 4 commits from zengweiran_branch into master 4 weeks ago

@ -1,17 +1,15 @@
<?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.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -20,40 +18,38 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
android:theme="@style/Theme.NotesMaster"
tools:targetApi="31">
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android: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>
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:theme="@style/Theme.NotesMaster"
android:exported="true">
<intent-filter>
<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 >
<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" />
@ -75,6 +71,15 @@
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"
@ -122,20 +127,27 @@
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
android:theme="@style/Theme.NotesMaster" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
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>
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<meta-data
android:name="android.app.default_searchable"

@ -1,6 +1,10 @@
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;
@ -8,11 +12,21 @@ 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) -> {
@ -21,4 +35,44 @@ public class MainActivity extends AppCompatActivity {
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();
}
}

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

@ -0,0 +1,344 @@
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 +
'}';
}
}
}

@ -140,6 +140,47 @@ public class Notes {
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
*/
@ -150,6 +191,11 @@ public class Notes {
*/
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");
/**
*
*/
@ -257,6 +303,12 @@ 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";
}
/**

@ -18,6 +18,7 @@ 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;
@ -48,7 +49,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* {@link #onUpgrade}
*/
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 8;
/**
*
@ -56,6 +57,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
*
* 1. {@link #NOTE}便
* 2. {@link #DATA}便 MIME
* 3. {@link #ATTACHMENT}便
*/
public interface TABLE {
/**
@ -67,6 +69,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
*
*/
public static final String DATA = "data";
/**
*
*/
public static final String ATTACHMENT = "note_attachment";
}
/**
@ -109,6 +116,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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" +
")";
/**
@ -136,6 +144,26 @@ 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>
@ -403,6 +431,20 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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>
@ -428,7 +470,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
@ -438,7 +480,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
*
* <p>
* note data
* note data
*
* @param db
*/
@ -448,6 +490,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createNoteTable(db);
// 创建数据表
createDataTable(db);
// 创建附件表
createAttachmentTable(db);
}
/**
@ -486,13 +530,37 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
// 4. 如果表结构在v3升级中发生较大变更需要重建触发器以确保兼容性
// 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);
}
// 5. 最终版本校验:确保所有升级步骤已执行完毕
// 8. 最终版本校验:确保所有升级步骤已执行完毕
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
@ -555,4 +623,148 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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);
}
}
}

@ -34,6 +34,7 @@ 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;
@ -88,6 +89,16 @@ public class NotesProvider extends ContentProvider {
*/
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
*/
@ -101,6 +112,8 @@ public class NotesProvider extends ContentProvider {
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
}
/**
@ -168,6 +181,15 @@ 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) {
@ -465,6 +487,14 @@ 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);
}
@ -524,6 +554,14 @@ 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);
}
@ -571,6 +609,14 @@ 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);
}

@ -1,135 +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.gtask.data;
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
/**
* MetaData - TaskGTask
*
* GTaskGTaskID
* Task
*/
public class MetaData extends Task {
private final static String TAG = MetaData.class.getSimpleName();
// 与当前元数据关联的GTask ID
private String mRelatedGid = null;
/**
*
*
* @param gid GTask ID
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 将GTask ID添加到元数据中
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
Log.e(TAG, "failed to put related gid");
}
// 将元数据以字符串形式存储到Task的notes字段中
setNotes(metaInfo.toString());
// 设置元数据任务的名称
setName(GTaskStringUtils.META_NOTE_NAME);
}
/**
* GTask ID
*
* @return GTask ID
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
*
*
* @return notestruefalse
*/
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
/**
* JSON
*
* @param js JSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
super.setContentByRemoteJSON(js);
// 如果有notes内容则解析获取关联的GTask ID
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;
}
}
}
/**
* JSON -
*
* @param js JSON
* @throws IllegalAccessError
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 元数据不支持从本地JSON设置内容
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON -
*
* @return JSON
* @throws IllegalAccessError
*/
@Override
public JSONObject getLocalJSONFromContent() {
// 元数据不支持获取本地JSON对象
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
* -
*
* @param c
* @return
* @throws IllegalAccessError
*/
@Override
public int getSyncAction(Cursor c) {
// 元数据不支持获取同步操作类型
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}

@ -1,222 +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.gtask.data;
import android.database.Cursor;
import org.json.JSONObject;
/**
* Node - GTask
*
* TaskListTaskMetaData
* GTask
*/
public abstract class Node {
/**
*
*/
public static final int SYNC_ACTION_NONE = 0;
/**
*
*/
public static final int SYNC_ACTION_ADD_REMOTE = 1;
/**
*
*/
public static final int SYNC_ACTION_ADD_LOCAL = 2;
/**
*
*/
public static final int SYNC_ACTION_DEL_REMOTE = 3;
/**
*
*/
public static final int SYNC_ACTION_DEL_LOCAL = 4;
/**
*
*/
public static final int SYNC_ACTION_UPDATE_REMOTE = 5;
/**
*
*/
public static final int SYNC_ACTION_UPDATE_LOCAL = 6;
/**
*
*/
public static final int SYNC_ACTION_UPDATE_CONFLICT = 7;
/**
*
*/
public static final int SYNC_ACTION_ERROR = 8;
/**
* Google TasksID
*/
private String mGid;
/**
*
*/
private String mName;
/**
*
*/
private long mLastModified;
/**
*
*/
private boolean mDeleted;
/**
* -
*/
public Node() {
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
/**
* JSON
*
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSON
*
* @param actionId ID
* @return JSON
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* JSON
*
* @param js Google TasksJSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
*
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSON
*
* @return JSON
*/
public abstract JSONObject getLocalJSONFromContent();
/**
*
*
* @param c
* @return 使SYNC_ACTION_*
*/
public abstract int getSyncAction(Cursor c);
/**
* Google TasksID
*
* @param gid ID
*/
public void setGid(String gid) {
this.mGid = gid;
}
/**
*
*
* @param name
*/
public void setName(String name) {
this.mName = name;
}
/**
*
*
* @param lastModified
*/
public void setLastModified(long lastModified) {
this.mLastModified = lastModified;
}
/**
*
*
* @param deleted
*/
public void setDeleted(boolean deleted) {
this.mDeleted = deleted;
}
/**
* Google TasksID
*
* @return ID
*/
public String getGid() {
return this.mGid;
}
/**
*
*
* @return
*/
public String getName() {
return this.mName;
}
/**
*
*
* @return
*/
public long getLastModified() {
return this.mLastModified;
}
/**
*
*
* @return
*/
public boolean getDeleted() {
return this.mDeleted;
}
}

@ -1,284 +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.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;
/**
* SqlData - SQLNotes
*
*
* 1. IDMIME
* 2. CursorJSON
* 3. 使ContentValues
* 4.
*/
public class SqlData {
private static final String TAG = SqlData.class.getSimpleName();
/**
* IDID
*/
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
};
/**
* PROJECTION_DATAID
*/
public static final int DATA_ID_COLUMN = 0;
/**
* PROJECTION_DATAMIME
*/
public static final int DATA_MIME_TYPE_COLUMN = 1;
/**
* PROJECTION_DATA
*/
public static final int DATA_CONTENT_COLUMN = 2;
/**
* PROJECTION_DATADATA1
*/
public static final int DATA_CONTENT_DATA_1_COLUMN = 3;
/**
* PROJECTION_DATADATA3
*/
public static final int DATA_CONTENT_DATA_3_COLUMN = 4;
/**
* 访Android ContentProviderContentResolver
*/
private ContentResolver mContentResolver;
/**
*
*/
private boolean mIsCreate;
/**
*
*/
private long mDataId;
/**
* MIME
*/
private String mDataMimeType;
/**
*
*/
private String mDataContent;
/**
* 1
*/
private long mDataContentData1;
/**
* 3
*/
private String mDataContentData3;
/**
* ContentValues
*/
private ContentValues mDiffDataValues;
/**
* - SqlData
*
* @param context ContentResolver
*/
public SqlData(Context context) {
mContentResolver = context.getContentResolver();
mIsCreate = true;
mDataId = INVALID_ID;
mDataMimeType = DataConstants.NOTE;
mDataContent = "";
mDataContentData1 = 0;
mDataContentData3 = "";
mDiffDataValues = new ContentValues();
}
/**
* - CursorSqlData
*
* @param context ContentResolver
* @param c Cursor
*/
public SqlData(Context context, Cursor c) {
mContentResolver = context.getContentResolver();
mIsCreate = false;
loadFromCursor(c);
mDiffDataValues = new ContentValues();
}
/**
* Cursor
*
* @param c Cursor
*/
private void loadFromCursor(Cursor c) {
mDataId = c.getLong(DATA_ID_COLUMN);
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
mDataContent = c.getString(DATA_CONTENT_COLUMN);
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
/**
* JSON
*
* @param js JSON
* @throws JSONException JSON
*/
public void setContent(JSONObject js) throws JSONException {
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
mDataId = dataId;
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
mDataMimeType = dataMimeType;
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
mDataContent = dataContent;
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
mDataContentData1 = dataContentData1;
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
mDataContentData3 = dataContentData3;
}
/**
* JSON
*
* @return JSON
* @throws JSONException JSON
*/
public JSONObject getContent() throws JSONException {
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
JSONObject js = new JSONObject();
js.put(DataColumns.ID, mDataId);
js.put(DataColumns.MIME_TYPE, mDataMimeType);
js.put(DataColumns.CONTENT, mDataContent);
js.put(DataColumns.DATA1, mDataContentData1);
js.put(DataColumns.DATA3, mDataContentData3);
return js;
}
/**
*
*
* @param noteId ID
* @param validateVersion
* @param version
* @throws ActionFailureException
*/
public void commit(long noteId, boolean validateVersion, long version) {
if (mIsCreate) {
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
} else {
if (mDiffDataValues.size() > 0) {
int result = 0;
if (!validateVersion) {
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues, null, null);
} else {
result = mContentResolver.update(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mDataId), mDiffDataValues,
" ? in (SELECT " + NoteColumns.ID + " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
String.valueOf(noteId), String.valueOf(version)
});
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
mDiffDataValues.clear();
mIsCreate = false;
}
/**
* ID
*
* @return
*/
public long getId() {
return mDataId;
}
}

@ -1,615 +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.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;
/**
* SqlNote -
*
* GTask
* CursorJSON
*/
public class SqlNote {
private static final String TAG = SqlNote.class.getSimpleName();
// 无效ID常量用于初始化和验证
private static final int INVALID_ID = -99999;
// 笔记查询投影数组,定义了从数据库查询笔记时返回的列
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, NoteColumns.ALERTED_DATE, NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE, NoteColumns.HAS_ATTACHMENT, NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT, NoteColumns.PARENT_ID, NoteColumns.SNIPPET, NoteColumns.TYPE,
NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE, NoteColumns.SYNC_ID,
NoteColumns.LOCAL_MODIFIED, NoteColumns.ORIGIN_PARENT_ID, NoteColumns.GTASK_ID,
NoteColumns.VERSION
};
// 投影数组中各列的索引常量定义
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;
// 内容解析器用于与ContentProvider交互
private ContentResolver mContentResolver;
// 是否为新创建的笔记
private boolean mIsCreate;
// 笔记ID
private long mId;
// 提醒日期
private long mAlertDate;
// 背景颜色ID
private int mBgColorId;
// 创建日期
private long mCreatedDate;
// 是否有附件
private int mHasAttachment;
// 修改日期
private long mModifiedDate;
// 父文件夹ID
private long mParentId;
// 笔记摘要
private String mSnippet;
// 笔记类型(普通笔记、文件夹等)
private int mType;
// 小部件ID
private int mWidgetId;
// 小部件类型
private int mWidgetType;
// 原始父文件夹ID
private long mOriginParent;
// 版本号,用于并发控制
private long mVersion;
// 差异内容值,用于记录需要更新的字段
private ContentValues mDiffNoteValues;
// 私有成员变量用于存储SqlData类型的数据列表
// 使用ArrayList作为数据结构可以动态存储SqlData对象
private ArrayList<SqlData> mDataList;
/**
*
*
* @param context 访
*/
public SqlNote(Context context) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = true; // 标记为新创建的笔记
mId = INVALID_ID; // 初始化为无效ID
mAlertDate = 0; // 无提醒日期
mBgColorId = ResourceParser.getDefaultBgId(context); // 使用默认背景颜色
mCreatedDate = System.currentTimeMillis(); // 创建日期设为当前时间
mHasAttachment = 0; // 初始没有附件
mModifiedDate = System.currentTimeMillis(); // 修改日期设为当前时间
mParentId = 0; // 默认父文件夹ID为0
mSnippet = ""; // 摘要为空
mType = Notes.TYPE_NOTE; // 默认类型为普通笔记
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID; // 无效小部件ID
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 无效小部件类型
mOriginParent = 0; // 原始父文件夹ID
mVersion = 0; // 初始版本号
mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值
mDataList = new ArrayList<SqlData>(); // 初始化数据列表
}
/**
* CursorSqlNote
*
* @param context
* @param c Cursor
*/
public SqlNote(Context context, Cursor c) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已存在的笔记
loadFromCursor(c); // 从Cursor加载数据
mDataList = new ArrayList<SqlData>(); // 初始化数据列表
if (mType == Notes.TYPE_NOTE) // 如果是普通笔记,加载其数据内容
loadDataContent();
mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值
}
/**
* IDSqlNote
*
* @param context
* @param id ID
*/
public SqlNote(Context context, long id) {
mContext = context;
mContentResolver = context.getContentResolver();
mIsCreate = false; // 标记为已存在的笔记
loadFromCursor(id); // 根据ID从数据库加载数据
mDataList = new ArrayList<SqlData>(); // 初始化数据列表
if (mType == Notes.TYPE_NOTE) // 如果是普通笔记,加载其数据内容
loadDataContent();
mDiffNoteValues = new ContentValues(); // 用于记录差异的内容值
}
/**
* ID
*
* @param id ID
*/
private void loadFromCursor(long id) {
Cursor c = null;
try {
// 查询指定ID的笔记数据
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] {
String.valueOf(id)
}, null);
if (c != null) {
c.moveToNext();
loadFromCursor(c); // 调用Cursor版本的loadFromCursor方法
} else {
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
if (c != null)
c.close(); // 确保Cursor被关闭
}
}
/**
* Cursor
*
* @param c Cursor
*/
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN);
mAlertDate = c.getLong(ALERTED_DATE_COLUMN);
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = c.getLong(CREATED_DATE_COLUMN);
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN);
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN);
mParentId = c.getLong(PARENT_ID_COLUMN);
mSnippet = c.getString(SNIPPET_COLUMN);
mType = c.getInt(TYPE_COLUMN);
mWidgetId = c.getInt(WIDGET_ID_COLUMN);
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN);
mVersion = c.getLong(VERSION_COLUMN);
}
/**
*
*
* mDataList
*/
private void loadDataContent() {
Cursor c = null;
mDataList.clear(); // 清空现有数据列表
try {
// 查询与当前笔记ID关联的所有数据项
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;
}
// 遍历Cursor创建SqlData对象并添加到数据列表
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(); // 确保Cursor被关闭
}
}
/**
* JSON
*
* @param js JSON
* @return
*/
public boolean setContent(JSONObject js) {
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 根据笔记类型进行不同处理
if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
Log.w(TAG, "cannot set system folder");
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// 文件夹类型只能更新摘要和类型
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
mDiffNoteValues.put(NoteColumns.SNIPPET, snippet);
}
mSnippet = snippet;
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
mDiffNoteValues.put(NoteColumns.TYPE, type);
}
mType = type;
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
// 普通笔记类型,需要处理所有字段和关联的数据
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 更新笔记基本信息
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;
// 尝试在现有数据列表中找到匹配的SqlData对象
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
if (dataId == temp.getId()) {
sqlData = temp;
}
}
}
// 如果找不到匹配的SqlData对象则创建新的
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
// 设置SqlData的内容
sqlData.setContent(data);
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
return true;
}
/**
* JSON
*
* @return JSONnull
*/
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();
// 根据笔记类型构建不同的JSON结构
if (mType == Notes.TYPE_NOTE) {
// 普通笔记类型,包含完整的笔记信息和关联数据
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
note.put(NoteColumns.BG_COLOR_ID, mBgColorId);
note.put(NoteColumns.CREATED_DATE, mCreatedDate);
note.put(NoteColumns.HAS_ATTACHMENT, mHasAttachment);
note.put(NoteColumns.MODIFIED_DATE, mModifiedDate);
note.put(NoteColumns.PARENT_ID, mParentId);
note.put(NoteColumns.SNIPPET, mSnippet);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
// 添加关联的数据项
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
} else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
// 文件夹或系统类型,只包含基本信息
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.TYPE, mType);
note.put(NoteColumns.SNIPPET, mSnippet);
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
return js;
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return null;
}
/**
* ID
*
* @param id ID
*/
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
/**
* GTask ID
*
* @param gid GTask ID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* ID
*
* @param syncId ID
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
*
*
* 0
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
/**
* ID
*
* @return ID
*/
public long getId() {
return mId;
}
/**
* ID
*
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
*
* @return
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
*
*
* @param validateVersion
* @throws ActionFailureException
* @throws IllegalStateException ID
*/
public void commit(boolean validateVersion) {
if (mIsCreate) {
// 新创建的笔记,执行插入操作
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID); // 移除无效ID
}
// 插入笔记到数据库
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
mId = Long.valueOf(uri.getPathSegments().get(1)); // 获取自动生成的ID
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
if (mId == 0) {
throw new IllegalStateException("Create thread id failed");
}
// 如果是普通笔记,提交其关联的数据
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, false, -1);
}
}
} else {
// 已存在的笔记,执行更新操作
if (mId <= 0 && mId != Notes.ID_ROOT_FOLDER && mId != Notes.ID_CALL_RECORD_FOLDER) {
Log.e(TAG, "No such note");
throw new IllegalStateException("Try to update note with invalid id");
}
if (mDiffNoteValues.size() > 0) {
mVersion ++; // 版本号递增
int result = 0;
if (!validateVersion) {
// 不验证版本,直接更新
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + NoteColumns.ID + "=?)", new String[] {
String.valueOf(mId)
});
} else {
// 验证版本,防止并发更新冲突
result = mContentResolver.update(Notes.CONTENT_NOTE_URI, mDiffNoteValues, "(" + NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
}
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
// 如果是普通笔记,提交其关联的数据
if (mType == Notes.TYPE_NOTE) {
for (SqlData sqlData : mDataList) {
sqlData.commit(mId, validateVersion, mVersion);
}
}
}
// 刷新本地信息,确保与数据库一致
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 清空差异内容值并标记为已存在
mDiffNoteValues.clear();
mIsCreate = false;
}
}

@ -1,470 +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.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;
/**
* Task - NodeGTask
*
*
* 1.
* 2.
* 3. GTask
* 4. JSON
* 5.
*/
public class Task extends Node {
private static final String TAG = Task.class.getSimpleName();
/**
*
*/
private boolean mCompleted;
/**
*
*/
private String mNotes;
/**
* JSON
*/
private JSONObject mMetaInfo;
/**
*
*/
private Task mPriorSibling;
/**
*
*/
private TaskList mParent;
/**
* -
*/
public Task() {
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
/**
* JSON
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js;
}
/**
* JSON
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js;
}
/**
* JSON
*
* @param js Google TasksJSON
* @throws ActionFailureException JSON
*/
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
}
}
}
/**
* JSON
*
* @param js JSON
*/
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
if (TextUtils.equals(data.getString(DataColumns.MIME_TYPE), DataConstants.NOTE)) {
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* JSON
*
* @return JSONnull
*/
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;
}
}
/**
*
*
* @param metaData MetaData
*/
public void setMetaInfo(MetaData metaData) {
if (metaData != null && metaData.getNotes() != null) {
try {
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
/**
*
*
* @param c
* @return
* - SYNC_ACTION_NONE:
* - SYNC_ACTION_UPDATE_LOCAL:
* - SYNC_ACTION_UPDATE_REMOTE:
* - SYNC_ACTION_UPDATE_CONFLICT:
* - SYNC_ACTION_ERROR:
*/
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;
}
/**
*
*
* @return truefalse
*/
public boolean isWorthSaving() {
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
/**
*
*
* @param completed
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
/**
*
*
* @param notes
*/
public void setNotes(String notes) {
this.mNotes = notes;
}
/**
*
*
* @param priorSibling
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
/**
*
*
* @param parent
*/
public void setParent(TaskList parent) {
this.mParent = parent;
}
/**
*
*
* @return
*/
public boolean getCompleted() {
return this.mCompleted;
}
/**
*
*
* @return
*/
public String getNotes() {
return this.mNotes;
}
/**
*
*
* @return
*/
public Task getPriorSibling() {
return this.mPriorSibling;
}
/**
*
*
* @return
*/
public TaskList getParent() {
return this.mParent;
}
}

@ -1,471 +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.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;
/**
* TaskList - NodeTask
*
*
* 1. ID
* 2. Task
* 3. GTask
* 4. JSON
*/
public class TaskList extends Node {
private static final String TAG = TaskList.class.getSimpleName();
/**
* Google Tasks
*/
private int mIndex;
/**
* Task
*/
private ArrayList<Task> mChildren;
/**
* - Task1
*/
public TaskList() {
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
/**
* JSON
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null");
entity.put(GTaskStringUtils.GTASK_JSON_ENTITY_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js;
}
/**
* JSON
*
* @param actionId ID
* @return JSON
* @throws ActionFailureException JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject();
try {
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName());
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js;
}
/**
* JSON
*
* @param js Google TasksJSON
* @throws ActionFailureException JSON
*/
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) {
try {
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
}
}
}
/**
* JSON
*
* @param js JSON
*/
public void setContentByLocalJSON(JSONObject js) {
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
} else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
if (folder.getLong(NoteColumns.ID) == Notes.ID_ROOT_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_DEFAULT);
else if (folder.getLong(NoteColumns.ID) == Notes.ID_CALL_RECORD_FOLDER)
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_CALL_NOTE);
else
Log.e(TAG, "invalid system folder");
} else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* JSON
*
* @return JSONnull
*/
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;
}
}
/**
*
*
* @param c
* @return
* - SYNC_ACTION_NONE:
* - SYNC_ACTION_UPDATE_LOCAL:
* - SYNC_ACTION_UPDATE_REMOTE:
* - SYNC_ACTION_ERROR:
*/
public int getSyncAction(Cursor c) {
try {
if (c.getInt(SqlNote.LOCAL_MODIFIED_COLUMN) == 0) {
// there is no local update
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// no update both side
return SYNC_ACTION_NONE;
} else {
// apply remote to local
return SYNC_ACTION_UPDATE_LOCAL;
}
} else {
// validate gtask id
if (!c.getString(SqlNote.GTASK_ID_COLUMN).equals(getGid())) {
Log.e(TAG, "gtask id doesn't match");
return SYNC_ACTION_ERROR;
}
if (c.getLong(SqlNote.SYNC_ID_COLUMN) == getLastModified()) {
// local modification only
return SYNC_ACTION_UPDATE_REMOTE;
} else {
// for folder conflicts, just apply local modification
return SYNC_ACTION_UPDATE_REMOTE;
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
}
return SYNC_ACTION_ERROR;
}
/**
*
*
* @return
*/
public int getChildTaskCount() {
return mChildren.size();
}
/**
*
*
* @param task
* @return
*/
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;
}
/**
*
*
* @param task
* @param index
* @return
*/
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;
}
/**
*
*
* @param task
* @return
*/
public boolean removeChildTask(Task task) {
boolean ret = false;
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task);
if (ret) {
// 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;
}
/**
*
*
* @param task
* @param index
* @return
*/
public boolean moveChildTask(Task task, int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
if (pos == index)
return true;
return (removeChildTask(task) && addChildTask(task, index));
}
/**
* Gid
*
* @param gid Gid
* @return Tasknull
*/
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
if (t.getGid().equals(gid)) {
return t;
}
}
return null;
}
/**
*
*
* @param task
* @return -1
*/
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
/**
*
*
* @param index
* @return Tasknull
*/
public Task getChildTaskByIndex(int index) {
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
}
return mChildren.get(index);
}
/**
* GidfindChildTaskByGid
*
* @param gid Gid
* @return Tasknull
*/
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
return task;
}
return null;
}
/**
*
*
* @return ArrayList
*/
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
/**
*
*
* @param index
*/
public void setIndex(int index) {
this.mIndex = index;
}
/**
*
*
* @return
*/
public int getIndex() {
return this.mIndex;
}
}

@ -1,55 +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.gtask.exception;
/**
* ActionFailureException - RuntimeExceptionGTask
*
* GTask
*
*
* @author MiCode Open Source Community
*/
public class ActionFailureException extends RuntimeException {
private static final long serialVersionUID = 4425249765923293627L;
/**
* ActionFailureException
*/
public ActionFailureException() {
super();
}
/**
* ActionFailureException
*
* @param paramString
*/
public ActionFailureException(String paramString) {
super(paramString);
}
/**
* ActionFailureException
*
* @param paramString
* @param paramThrowable
*/
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -1,55 +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.gtask.exception;
/**
* NetworkFailureException - ExceptionGTask
*
* GTaskGoogle Tasks
*
*
* @author MiCode Open Source Community
*/
public class NetworkFailureException extends Exception {
private static final long serialVersionUID = 2107610287180234136L;
/**
* NetworkFailureException
*/
public NetworkFailureException() {
super();
}
/**
* NetworkFailureException
*
* @param paramString
*/
public NetworkFailureException(String paramString) {
super(paramString);
}
/**
* NetworkFailureException
*
* @param paramString
* @param paramThrowable IOException
*/
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}

@ -1,207 +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.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;
/**
* GTaskASyncTask - AsyncTaskGoogle Tasks
*
* 线Google TasksUI线
*
* -
* -
* - 广
* -
* -
*
* @author MiCode Open Source Community
*/
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
/**
*
*/
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
/**
* OnCompleteListener -
*
*
*/
public interface OnCompleteListener {
/**
*
*/
void onComplete();
}
private Context mContext;
private NotificationManager mNotifiManager;
private GTaskManager mTaskManager;
private OnCompleteListener mOnCompleteListener;
/**
* GTaskASyncTask
*
* @param context
* @param listener
*/
public GTaskASyncTask(Context context, OnCompleteListener listener) {
mContext = context;
mOnCompleteListener = listener;
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
}
/**
*
*
* GTaskManagercancelSync
*/
public void cancelSync() {
mTaskManager.cancelSync();
}
/**
*
*
* UI线
*
* @param message
*/
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
/**
*
*
*
*
* @param tickerId ID
* @param content
*/
private void showNotification(int tickerId, String content) {
// 1. 创建通知构建器(替代旧构造函数)
Notification.Builder builder = new Notification.Builder(mContext)
.setSmallIcon(R.drawable.notification) // 替代旧的第一个参数
.setTicker(mContext.getString(tickerId)) // 替代旧的第二个参数
.setWhen(System.currentTimeMillis()) // 替代旧的第三个参数
.setDefaults(Notification.DEFAULT_LIGHTS) // 替代 notification.defaults
.setAutoCancel(true) // 替代 notification.flags = FLAG_AUTO_CANCEL
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content);
// 2. 创建 PendingIntent
Intent intent;
if (tickerId != R.string.ticker_success) {
intent = new Intent(mContext, NotesPreferenceActivity.class);
} else {
intent = new Intent(mContext, NotesListActivity.class);
}
// Android 12+ 需要 FLAG_IMMUTABLE 或 FLAG_MUTABLE
PendingIntent pendingIntent = PendingIntent.getActivity(
mContext, 0, intent, PendingIntent.FLAG_IMMUTABLE);
// 3. 设置意图
builder.setContentIntent(pendingIntent);
// 4. 构建并发送通知
Notification notification = builder.build();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
/**
* 线
*
* AsyncTask线Google Tasks
*
* @param unused 使
* @return GTaskManager
*/
@Override
protected Integer doInBackground(Void... unused) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
return mTaskManager.sync(mContext, this);
}
/**
* UI线
*
* 线publishProgressUI线
*
* @param progress
*/
@Override
protected void onProgressUpdate(String... progress) {
showNotification(R.string.ticker_syncing, progress[0]);
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
/**
* UI线
*
* 线doInBackgroundUI线
*
* @param result GTaskManager
*/
@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();
}
}
}

@ -1,694 +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.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;
/**
* GTaskClient - Google Tasks使
*
* Google Tasks
* HTTPJSONGoogle Tasks API
*
* @author MiCode Open Source Community
*/
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;
}
/**
* GTaskClient
*
* 线GTaskClient
*
*
* @return GTaskClient
*/
public static synchronized GTaskClient getInstance() {
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* Google Tasks
*
* Googlecookie
*
*
*
* @param activity Activity
* @return truefalse
*/
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);
}
try (InputStream input = entity.getContent();
InputStream finalInput = (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip"))
? new GZIPInputStream(input)
: (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate"))
? new InflaterInputStream(input, new Inflater(true))
: input;
InputStreamReader isr = new InputStreamReader(finalInput);
BufferedReader br = new BufferedReader(isr)) {
StringBuilder sb = new StringBuilder();
String buff;
while ((buff = br.readLine()) != null) {
sb.append(buff);
}
return sb.toString();
}
}
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");
}
}
/**
* Google Tasks
*
* Google Tasks
* IDGID
*
* @param task
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
* Google Tasks
*
* Google Tasks
* IDGID
*
* @param tasklist
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
*
*
* Google Tasks
*
*
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
}
/**
*
*
* 10
*
*
* @param node
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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()));
}
}
/**
*
*
*
* 1.
* 2.
*
* @param task
* @param preParent
* @param curParent
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
*
*
* Google Tasks
*
* @param node
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
*
*
* Google Tasks
*
* @return JSONArray
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
*
*
* Google Tasks
*
* @param listGid ID
* @return JSONArray
* @throws NetworkFailureException
* @throws ActionFailureException
*/
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");
}
}
/**
* 使Google
*
* @return 使Google
*/
public Account getSyncAccount() {
return mAccount;
}
/**
*
*
*
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}

@ -1,871 +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.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;
/**
* GTaskManager - Google Tasks使
*
* Google Tasks
*
* -
* -
* -
* -
* -
*
* @author MiCode Open Source Community
*/
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>();
}
/**
* GTaskManager
*
* 线GTaskManager
*
*
* @return GTaskManager
*/
public static synchronized GTaskManager getInstance() {
if (mInstance == null) {
mInstance = new GTaskManager();
}
return mInstance;
}
/**
* Activity
*
* GoogleActivity
*
* @param activity Activity
*/
public synchronized void setActivityContext(Activity activity) {
// used for getting authtoken
mActivity = activity;
}
/**
* Google Tasks
*
*
* 1. Google Tasks
* 2.
* 3.
* 4.
*
* @param context
* @param asyncTask
* @return
* - STATE_SUCCESS
* - STATE_NETWORK_ERROR
* - STATE_INTERNAL_ERROR
* - STATE_SYNC_IN_PROGRESS
* - STATE_SYNC_CANCELLED
*/
public int sync(Context context, GTaskASyncTask asyncTask) {
if (mSyncing) {
Log.d(TAG, "Sync is in progress");
return STATE_SYNC_IN_PROGRESS;
}
mContext = context;
mContentResolver = mContext.getContentResolver();
mSyncing = true;
mCancelled = false;
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
try {
GTaskClient client = GTaskClient.getInstance();
client.resetUpdateArray();
// login google task
if (!mCancelled) {
if (!client.login(mActivity)) {
throw new NetworkFailureException("login google task failed");
}
}
// get the task list from google
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_init_list));
initGTaskList();
// do content sync work
asyncTask.publishProgess(mContext.getString(R.string.sync_progress_syncing));
syncContent();
} catch (NetworkFailureException e) {
Log.e(TAG, e.toString());
return STATE_NETWORK_ERROR;
} catch (ActionFailureException e) {
Log.e(TAG, e.toString());
return STATE_INTERNAL_ERROR;
} catch (Exception e) {
Log.e(TAG, e.toString());
e.printStackTrace();
return STATE_INTERNAL_ERROR;
} finally {
mGTaskListHashMap.clear();
mGTaskHashMap.clear();
mMetaHashMap.clear();
mLocalDeleteIdMap.clear();
mGidToNid.clear();
mNidToGid.clear();
mSyncing = false;
}
return mCancelled ? STATE_SYNC_CANCELLED : STATE_SUCCESS;
}
private void initGTaskList() throws NetworkFailureException {
if (mCancelled)
return;
GTaskClient client = GTaskClient.getInstance();
try {
JSONArray jsTaskLists = client.getTaskLists();
// init meta list first
mMetaList = null;
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name
.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX + GTaskStringUtils.FOLDER_META)) {
mMetaList = new TaskList();
mMetaList.setContentByRemoteJSON(object);
// load meta data
JSONArray jsMetas = client.getTaskList(gid);
for (int j = 0; j < jsMetas.length(); j++) {
object = (JSONObject) jsMetas.getJSONObject(j);
MetaData metaData = new MetaData();
metaData.setContentByRemoteJSON(object);
if (metaData.isWorthSaving()) {
mMetaList.addChildTask(metaData);
if (metaData.getGid() != null) {
mMetaHashMap.put(metaData.getRelatedGid(), metaData);
}
}
}
}
}
// create meta list if not existed
if (mMetaList == null) {
mMetaList = new TaskList();
mMetaList.setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META);
GTaskClient.getInstance().createTaskList(mMetaList);
}
// init task list
for (int i = 0; i < jsTaskLists.length(); i++) {
JSONObject object = jsTaskLists.getJSONObject(i);
String gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
String name = object.getString(GTaskStringUtils.GTASK_JSON_NAME);
if (name.startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX)
&& !name.equals(GTaskStringUtils.MIUI_FOLDER_PREFFIX
+ GTaskStringUtils.FOLDER_META)) {
TaskList tasklist = new TaskList();
tasklist.setContentByRemoteJSON(object);
mGTaskListHashMap.put(gid, tasklist);
mGTaskHashMap.put(gid, tasklist);
// load tasks
JSONArray jsTasks = client.getTaskList(gid);
for (int j = 0; j < jsTasks.length(); j++) {
object = (JSONObject) jsTasks.getJSONObject(j);
gid = object.getString(GTaskStringUtils.GTASK_JSON_ID);
Task task = new Task();
task.setContentByRemoteJSON(object);
if (task.isWorthSaving()) {
task.setMetaInfo(mMetaHashMap.get(gid));
tasklist.addChildTask(task);
mGTaskHashMap.put(gid, task);
}
}
}
}
} catch (JSONException e) {
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("initGTaskList: handing JSONObject failed");
}
}
private void syncContent() throws NetworkFailureException {
int syncType;
Cursor c = null;
String gid;
Node node;
mLocalDeleteIdMap.clear();
if (mCancelled) {
return;
}
// for local deleted note
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLDER)
}, 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_FOLDER)
}, 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_FOLDER)
}, 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_FOLDER)
}, 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;
}
}
}
/**
* 使Google
*
* @return 使Google
*/
public String getSyncAccount() {
return GTaskClient.getInstance().getSyncAccount().name;
}
/**
*
*
* 使
*/
public void cancelSync() {
mCancelled = true;
}
}

@ -1,232 +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.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;
/**
* GTaskSyncService - Android ServiceGoogle Tasks
*
* Google Tasks广
* GTaskASyncTask广UI
*
* @author MiCode Open Source Community
*/
public class GTaskSyncService extends Service {
/**
* Intent
*/
public final static String ACTION_STRING_NAME = "sync_action_type";
/**
*
*/
public final static int ACTION_START_SYNC = 0;
/**
*
*/
public final static int ACTION_CANCEL_SYNC = 1;
/**
*
*/
public final static int ACTION_INVALID = 2;
/**
* 广Action
*/
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 = "";
/**
*
*
* GTaskASyncTask
*
*/
private void startSync() {
if (mSyncTask == null) {
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
public void onComplete() {
mSyncTask = null;
sendBroadcast("");
stopSelf();
}
});
sendBroadcast("");
mSyncTask.execute();
}
}
/**
*
*
* cancelSync
*/
private void cancelSync() {
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
@Override
public void onCreate() {
mSyncTask = null;
}
/**
*
*
* Intent
*
* @param intent Intent
* @param flags
* @param startId ID
* @return START_STICKY
*/
@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();
}
}
/**
*
*
* null
*
* @param intent Intent
* @return IBindernull
*/
public IBinder onBind(Intent intent) {
return null;
}
/**
* 广
*
* 广广
*
* @param msg
*/
public void sendBroadcast(String msg) {
mSyncProgress = msg;
Intent intent = new Intent(GTASK_SERVICE_BROADCAST_NAME);
intent.putExtra(GTASK_SERVICE_BROADCAST_IS_SYNCING, mSyncTask != null);
intent.putExtra(GTASK_SERVICE_BROADCAST_PROGRESS_MSG, msg);
sendBroadcast(intent);
}
/**
*
*
* ActivityGTaskSyncService
*
* @param activity Activity
*/
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);
}
/**
*
*
* ContextGTaskSyncService
*
* @param context
*/
public static void cancelSync(Context context) {
Intent intent = new Intent(context, GTaskSyncService.class);
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
context.startService(intent);
}
/**
*
*
*
*
* @return truefalse
*/
public static boolean isSyncing() {
return mSyncTask != null;
}
/**
*
*
*
*
* @return
*/
public static String getProgressString() {
return mSyncProgress;
}
}

@ -29,8 +29,13 @@ 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 - 使
@ -46,6 +51,10 @@ public class WorkingNote {
* Note
*/
private Note mNote;
/**
*
*/
private AttachmentManager mAttachmentManager;
/**
* ID
*/
@ -145,6 +154,7 @@ public class WorkingNote {
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
@ -158,6 +168,7 @@ public class WorkingNote {
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
mAttachmentManager = new AttachmentManager(context);
loadNote();
}
@ -379,6 +390,61 @@ 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

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

@ -249,7 +249,7 @@ public class BackupUtils {
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
@ -300,60 +300,9 @@ public class BackupUtils {
return STATE_SYSTEM_ERROR;
}
// 第一部分:导出文件夹及其中的笔记
// 查询所有文件夹(排除回收站,但包含通话记录文件夹)
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_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();
}
// 第二部分:导出根目录下的笔记(不属于任何文件夹的笔记)
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();
}
// 关闭输出流
ps.close();
return STATE_SUCCESS;
}
/**

@ -376,4 +376,46 @@ public class DataUtils {
}
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;
}
}

@ -19,6 +19,7 @@ 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;
@ -66,7 +67,7 @@ public class ElderModeUtils {
// 只增大用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() * ELDER_MODE_FONT_SCALE);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() * ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View
@ -84,9 +85,10 @@ public class ElderModeUtils {
* @param view View
*/
public static void clearElderMode(Context context, View view) {
if (!isElderModeEnabled(context)) {
return;
}
// 移除这个检查,因为清除操作应该在老年人模式禁用时执行
// if (!isElderModeEnabled(context)) {
// return;
// }
// 只对用户输入的内容应用字体恢复,系统信息保持不变
if (view instanceof TextView) {
@ -96,7 +98,7 @@ public class ElderModeUtils {
// 只恢复用户输入内容的字体
if (id == R.id.tv_title || // 笔记列表中的标题(用户输入内容)
id == R.id.note_edit_view) { // 编辑界面中的内容(用户输入)
textView.setTextSize(textView.getTextSize() / ELDER_MODE_FONT_SCALE);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textView.getTextSize() / ELDER_MODE_FONT_SCALE);
}
} else if (view instanceof ViewGroup) {
// 递归处理ViewGroup中的所有子View

@ -0,0 +1,148 @@
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;
}
}

@ -0,0 +1,97 @@
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;
}
}

@ -0,0 +1,74 @@
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;
}
}

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

@ -27,10 +27,16 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
@ -52,6 +58,7 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@ -70,19 +77,38 @@ import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
import net.micode.notes.data.AttachmentManager;
import net.micode.notes.tool.PermissionHelper;
import net.micode.notes.security.PasswordManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;
import com.google.android.material.appbar.MaterialToolbar;
/**
* Activity - 便
* OnClickListener, NoteSettingChangedListener, OnTextViewChangeListener
*
*/
public class NoteEditActivity extends Activity implements OnClickListener,
public class NoteEditActivity extends AppCompatActivity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
/**
@ -156,17 +182,27 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private String mUserQuery; // 用户搜索查询(用于高亮显示)
private Pattern mPattern; // 用于高亮搜索结果的模式
// 附件相关
private LinearLayout mAttachmentContainer; // 附件容器
private static final int REQUEST_GALLERY = 1001;
private static final int REQUEST_CAMERA = 1002;
private static final int REQUEST_PERMISSION_CAMERA = 1003;
private static final int REQUEST_PERMISSION_STORAGE = 1004;
private static final int REQUEST_CAMERA_PREVIEW = 1005;
private String mCurrentPhotoPath; // 相机拍照临时文件路径
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.note_edit); // 设置布局文件
initResources(); // 初始化资源
// 初始化Activity状态如果初始化失败则结束Activity
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
initResources(); // 初始化资源
}
/**
@ -215,18 +251,32 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
} else {
// 加载笔记
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load note failed with note id" + noteId);
finish();
return false;
}
// 在后台线程加载笔记避免阻塞UI线程
new AsyncTask<Long, Void, WorkingNote>() {
@Override
protected WorkingNote doInBackground(Long... params) {
long id = params[0];
try {
return WorkingNote.load(NoteEditActivity.this, id);
} catch (Exception e) {
Log.e(TAG, "load note failed with note id" + id, e);
return null;
}
}
@Override
protected void onPostExecute(WorkingNote result) {
if (result != null) {
mWorkingNote = result;
setupWorkingNoteAndInitializeUI(Intent.ACTION_VIEW);
} else {
Log.e(TAG, "load note failed");
finish();
}
}
}.execute(noteId);
return true;
}
// 隐藏软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
// 处理新建或编辑笔记的请求
else if(TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, intent.getAction())) {
@ -250,45 +300,167 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 检查是否已存在相同的通话记录笔记
if ((noteId = DataUtils.getNoteIdByPhoneNumberAndCallDate(getContentResolver(),
phoneNumber, callDate)) > 0) {
mWorkingNote = WorkingNote.load(this, noteId);
if (mWorkingNote == null) {
Log.e(TAG, "load call note failed with note id" + noteId);
finish();
return false;
}
// 在后台线程加载通话记录笔记
new AsyncTask<Long, Void, WorkingNote>() {
@Override
protected WorkingNote doInBackground(Long... params) {
long id = params[0];
try {
return WorkingNote.load(NoteEditActivity.this, id);
} catch (Exception e) {
Log.e(TAG, "load call note failed with note id" + id, e);
return null;
}
}
@Override
protected void onPostExecute(WorkingNote result) {
if (result != null) {
mWorkingNote = result;
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
} else {
Log.e(TAG, "load call note failed");
finish();
}
}
}.execute(noteId);
} else {
// 创建新的通话记录笔记
// 创建新的通话记录笔记无需数据库操作直接在UI线程处理
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId,
widgetType, bgResId);
mWorkingNote.convertToCallNote(phoneNumber, callDate);
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
}
} else {
// 创建普通笔记
// 创建普通笔记无需数据库操作直接在UI线程处理
mWorkingNote = WorkingNote.createEmptyNote(this, folderId, widgetId, widgetType,
bgResId);
setupWorkingNoteAndInitializeUI(Intent.ACTION_INSERT_OR_EDIT);
}
// 显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
} else {
Log.e(TAG, "Intent not specified action, should not support");
finish();
return false;
}
// 设置笔记设置变化监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
}
/**
* WorkingNoteUI
*/
private void setupWorkingNoteAndInitializeUI(String action) {
if (mWorkingNote != null) {
// 设置笔记设置变化监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
// 根据不同的操作类型设置软键盘模式
if (TextUtils.equals(Intent.ACTION_VIEW, action)) {
// 检查便签是否加锁
if (isNoteLocked()) {
showPasswordDialogForLockedNote();
} else {
// 隐藏软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
// 初始化UI界面
initNoteScreen();
// 加载附件
loadAttachments();
}
} else if (TextUtils.equals(Intent.ACTION_INSERT_OR_EDIT, action)) {
// 显示软键盘
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
// 初始化UI界面
initNoteScreen();
// 加载附件
loadAttachments();
}
}
}
/**
*
*/
private boolean isNoteLocked() {
try {
// 查询笔记的加锁状态
String[] projection = new String[] { Notes.NoteColumns.IS_LOCKED };
Cursor cursor = getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()),
projection, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
int isLocked = cursor.getInt(0);
return isLocked == 1;
}
} finally {
cursor.close();
}
}
} catch (Exception e) {
Log.e(TAG, "Error checking note lock status", e);
}
return false;
}
/**
* 便
*/
private void showPasswordDialogForLockedNote() {
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(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
etPassword.setHint(R.string.hint_enter_password);
etPassword.setText("");
builder.setTitle(R.string.title_locked_note);
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(NoteEditActivity.this, R.string.error_password_empty, Toast.LENGTH_SHORT).show();
return;
}
if (PasswordManager.verifyPassword(NoteEditActivity.this, password)) {
// 密码验证通过初始化UI
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
| WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
initNoteScreen();
loadAttachments();
} else {
Toast.makeText(NoteEditActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show();
finish();
}
}
});
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
});
builder.show();
builder.setCancelable(false);
}
@Override
protected void onResume() {
super.onResume();
initNoteScreen(); // 初始化笔记界面
// 每次恢复时应用老年人模式
ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content));
// 只有当mWorkingNote已经初始化完成时才初始化界面
if (mWorkingNote != null) {
initNoteScreen(); // 初始化笔记界面(已包含老年人模式应用)
}
}
/**
@ -327,6 +499,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 显示闹钟提醒头部
showAlertHeader();
// 在设置字体外观后应用老年人模式,确保老年人模式的字体设置不被覆盖
ElderModeUtils.applyElderMode(this, mNoteEditor);
}
/**
@ -412,6 +587,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
*/
private void initResources() {
// 设置Material Toolbar作为ActionBar
MaterialToolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
mHeadViewPanel = findViewById(R.id.note_title);
mNoteHeaderHolder = new HeadViewHolder();
mNoteHeaderHolder.tvModified = (TextView) findViewById(R.id.tv_modified_date);
@ -450,6 +630,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList = (LinearLayout) findViewById(R.id.note_edit_list);
// 初始化附件容器
mAttachmentContainer = (LinearLayout) findViewById(R.id.note_attachment_container);
// 应用老年人模式
ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content));
}
@ -568,6 +751,19 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
}
/**
*
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (mWorkingNote.getFolderId() == Notes.ID_CALL_RECORD_FOLDER) {
getMenuInflater().inflate(R.menu.call_note_edit, menu); // 通话记录笔记菜单
} else {
getMenuInflater().inflate(R.menu.note_edit, menu); // 普通笔记菜单
}
return true;
}
/**
*
*/
@ -638,6 +834,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.setCheckListMode(mWorkingNote.getCheckListMode() == 0 ?
TextNote.MODE_CHECK_LIST : 0);
break;
case R.id.menu_insert_image:
handleInsertImage(); // 插入图片
break;
case R.id.menu_share:
getWorkingText(); // 获取工作文本
sendTo(this, mWorkingNote.getContent()); // 分享笔记
@ -738,9 +937,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/**
*
* : Google Sync , false
*/
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
return false;
}
/**
@ -1221,4 +1421,293 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
// ==================== 附件相关方法 ====================
/**
*
*/
private void loadAttachments() {
if (mWorkingNote == null || mWorkingNote.getNoteId() <= 0) {
return;
}
List<AttachmentManager.Attachment> attachments = mWorkingNote.getAttachments();
if (attachments.isEmpty()) {
mAttachmentContainer.setVisibility(View.GONE);
return;
}
mAttachmentContainer.setVisibility(View.VISIBLE);
mAttachmentContainer.removeAllViews();
for (AttachmentManager.Attachment attachment : attachments) {
addAttachmentView(attachment);
}
}
/**
*
* @param attachment
*/
private void addAttachmentView(AttachmentManager.Attachment attachment) {
View view = LayoutInflater.from(this).inflate(R.layout.image_item, mAttachmentContainer, false);
ImageView imageView = view.findViewById(R.id.image_view);
ImageButton deleteButton = view.findViewById(R.id.btn_delete);
// 使用Glide加载图片
Glide.with(this)
.load(new File(attachment.filePath))
.override(800, 800)
.centerCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.into(imageView);
// 长按删除
view.setOnLongClickListener(v -> {
new AlertDialog.Builder(this)
.setTitle(R.string.delete_image)
.setMessage(R.string.confirm_delete_image)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
if (mWorkingNote.deleteAttachment(attachment.id)) {
mAttachmentContainer.removeView(view);
if (mAttachmentContainer.getChildCount() == 0) {
mAttachmentContainer.setVisibility(View.GONE);
}
showToast(R.string.image_deleted);
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
});
// 删除按钮点击
deleteButton.setOnClickListener(v -> view.performLongClick());
mAttachmentContainer.addView(view);
}
/**
*
*/
private void handleInsertImage() {
ImageInsertDialogFragment dialog = new ImageInsertDialogFragment();
dialog.setOnImageInsertListener(new ImageInsertDialogFragment.OnImageInsertListener() {
@Override
public void onGallerySelected() {
requestGalleryPermission();
}
@Override
public void onCameraSelected() {
requestCameraPermission();
}
});
dialog.show(getFragmentManager(), ImageInsertDialogFragment.TAG);
}
/**
*
*/
private void requestGalleryPermission() {
String[] permissions;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
permissions = new String[]{PermissionHelper.Permissions.READ_MEDIA_IMAGES};
} else {
permissions = new String[]{PermissionHelper.Permissions.READ_EXTERNAL_STORAGE};
}
if (PermissionHelper.arePermissionsGranted(this, permissions)) {
openGallery();
} else {
PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_STORAGE);
}
}
/**
*
*/
private void requestCameraPermission() {
String[] permissions = {PermissionHelper.Permissions.CAMERA};
if (PermissionHelper.arePermissionsGranted(this, permissions)) {
openCamera();
} else {
PermissionHelper.requestPermissions(this, permissions, REQUEST_PERMISSION_CAMERA);
}
}
/**
*
*/
private void openGallery() {
Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_GALLERY);
}
/**
*
*/
private void openCamera() {
Intent takePictureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
File photoFile = createImageFile();
if (photoFile != null) {
mCurrentPhotoPath = photoFile.getAbsolutePath();
try {
Uri photoURI = FileProvider.getUriForFile(this,
getApplicationContext().getPackageName() + ".fileprovider",
photoFile);
takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
startActivityForResult(takePictureIntent, REQUEST_CAMERA);
} catch (Exception e) {
Log.e(TAG, "Failed to get URI from FileProvider", e);
}
} else {
Log.e(TAG, "Failed to create image file");
}
}
/**
*
* @return null
*/
private File createImageFile() {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
try {
File image = File.createTempFile(imageFileName, ".jpg", storageDir);
return image;
} catch (IOException e) {
Log.e(TAG, "Failed to create image file", e);
return null;
}
}
/**
*
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionHelper.handlePermissionResult(requestCode, permissions, grantResults,
new PermissionHelper.OnPermissionResultListener() {
@Override
public void onAllGranted(int requestCode, List<String> grantedPermissions) {
if (requestCode == REQUEST_PERMISSION_STORAGE) {
openGallery();
} else if (requestCode == REQUEST_PERMISSION_CAMERA) {
openCamera();
}
}
@Override
public void onDenied(int requestCode, List<String> grantedPermissions, List<String> deniedPermissions) {
showToast(R.string.permission_denied);
}
});
}
/**
* Activity/
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != RESULT_OK) {
return;
}
if (requestCode == REQUEST_GALLERY && data != null) {
Uri selectedImage = data.getData();
if (selectedImage != null) {
processSelectedImage(selectedImage, Notes.ATTACHMENT_TYPE_GALLERY);
}
} else if (requestCode == REQUEST_CAMERA) {
if (mCurrentPhotoPath != null) {
File photoFile = new File(mCurrentPhotoPath);
if (photoFile.exists()) {
showCameraPreviewDialog(photoFile);
}
}
}
}
/**
*
* @param imageUri URI
* @param type
*/
private void processSelectedImage(Uri imageUri, int type) {
try {
File sourceFile = new File(imageUri.getPath());
if (!sourceFile.exists()) {
// 如果直接路径不存在尝试通过ContentResolver获取
sourceFile = getFileFromUri(imageUri);
}
if (sourceFile != null && sourceFile.exists()) {
long attachmentId = mWorkingNote.addAttachment(type, sourceFile);
if (attachmentId > 0) {
loadAttachments();
showToast(R.string.image_added);
} else {
showToast(R.string.failed_to_add_image);
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to process image", e);
showToast(R.string.failed_to_add_image);
}
}
/**
* URI
* @param uri URI
* @return null
*/
private File getFileFromUri(Uri uri) {
// 简化实现对于content:// URI直接复制到临时文件
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
if (inputStream == null) return null;
File tempFile = File.createTempFile("temp_image", ".jpg", getCacheDir());
FileOutputStream outputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
inputStream.close();
return tempFile;
} catch (IOException e) {
Log.e(TAG, "Failed to get file from URI", e);
return null;
}
}
/**
*
* @param photoFile
*/
private void showCameraPreviewDialog(File photoFile) {
CameraPreviewDialogFragment dialog = new CameraPreviewDialogFragment();
dialog.setPhotoFile(photoFile);
dialog.setOnCameraPreviewListener(new CameraPreviewDialogFragment.OnCameraPreviewListener() {
@Override
public void onConfirm(File photoFile) {
// 确认使用照片,添加到附件
processSelectedImage(Uri.fromFile(photoFile), Notes.ATTACHMENT_TYPE_CAMERA);
}
@Override
public void onRetake() {
// 重拍,重新打开相机
openCamera();
}
});
dialog.show(getFragmentManager(), CameraPreviewDialogFragment.TAG);
}
}

@ -46,6 +46,7 @@ public class NoteItemData {
NoteColumns.TYPE, // 类型(笔记、文件夹、系统文件夹等)
NoteColumns.WIDGET_ID, // 小部件ID
NoteColumns.WIDGET_TYPE, // 小部件类型
NoteColumns.IS_LOCKED, // 是否加锁
};
// 字段索引常量定义对应PROJECTION数组中的位置
@ -61,6 +62,7 @@ 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
@ -75,6 +77,7 @@ public class NoteItemData {
private int mType; // 笔记类型
private int mWidgetId; // 关联的小部件ID
private int mWidgetType; // 小部件类型
private boolean mIsLocked; // 是否加锁
// 通话记录相关字段
private String mName; // 联系人姓名
@ -113,6 +116,7 @@ public class NoteItemData {
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 = "";
@ -344,6 +348,14 @@ public class NoteItemData {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
* 便
* @return 便true
*/
public boolean isLocked() {
return mIsLocked;
}
/**
*
* @param cursor

@ -65,15 +65,18 @@ import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ElderModeUtils;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.security.PasswordManager;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
@ -88,7 +91,7 @@ import java.util.HashSet;
* Activity
*
*/
public class NotesListActivity extends Activity implements OnClickListener, OnItemLongClickListener {
public class NotesListActivity extends AppCompatActivity implements OnClickListener, OnItemLongClickListener {
// 查询令牌常量
private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0; // 查询文件夹内笔记列表的令牌
private static final int FOLDER_LIST_QUERY_TOKEN = 1; // 查询文件夹列表的令牌
@ -97,6 +100,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private static final int MENU_FOLDER_DELETE = 0; // 文件夹删除菜单项
private static final int MENU_FOLDER_VIEW = 1; // 文件夹查看菜单项
private static final int MENU_FOLDER_CHANGE_NAME = 2; // 文件夹改名菜单项
private static final int MENU_LOCK_NOTE = 3; // 便签加锁菜单项
private static final int MENU_UNLOCK_NOTE = 4; // 便签解锁菜单项
private static final int MENU_FOLDER_ENCRYPT = 3; // 文件夹加密菜单项
private static final int MENU_FOLDER_DECRYPT = 4; // 文件夹取消加密菜单项
@ -121,7 +126,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// 笔记列表视图
private ListView mNotesListView;
// 新建笔记按钮
private Button mAddNewNote;
private View mAddNewNote;
// 触摸事件分发标记
private boolean mDispatch;
// 触摸起始Y坐标
@ -301,12 +306,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
mAddNewNote = (Button) findViewById(R.id.btn_new_note);
mAddNewNote = findViewById(R.id.fab_new_note);
mAddNewNote.setOnClickListener(this);
mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener()); // 设置特殊触摸监听器
mDispatch = false;
mDispatchY = 0;
mOriginY = 0;
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表
mModeCallBack = new ModeCallback(); // 多选模式回调
@ -339,6 +346,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mMoveMenu.setVisible(true);
mMoveMenu.setOnMenuItemClickListener(this);
}
MenuItem lockMenu = menu.findItem(R.id.lock);
MenuItem unlockMenu = menu.findItem(R.id.unlock);
if (lockMenu != null) {
lockMenu.setOnMenuItemClickListener(this);
}
if (unlockMenu != null) {
unlockMenu.setOnMenuItemClickListener(this);
}
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true); // 进入多选模式
mNotesListView.setLongClickable(false); // 禁用长按
@ -451,6 +466,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// 批量移动到文件夹
startQueryDestinationFolders();
break;
case R.id.lock:
// 批量加锁
handleBatchLockNotes(true);
break;
case R.id.unlock:
// 批量解锁
handleBatchLockNotes(false);
break;
default:
return false;
}
@ -757,7 +780,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_new_note:
case R.id.fab_new_note:
createNewNote(); // 创建新笔记
break;
default:
@ -944,6 +967,24 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
};
/**
* 便
*/
private final OnCreateContextMenuListener mNoteOnCreateContextMenuListener = new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet()); // 设置菜单标题为便签标题
// 根据锁状态显示不同的菜单项
if (mFocusNoteDataItem.isLocked()) {
menu.add(0, MENU_UNLOCK_NOTE, 0, R.string.menu_unlock);
} else {
menu.add(0, MENU_LOCK_NOTE, 0, R.string.menu_lock);
}
}
}
};
@Override
public void onContextMenuClosed(Menu menu) {
if (mNotesListView != null) {
@ -989,6 +1030,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// 取消加密文件夹
decryptFolder(mFocusNoteDataItem.getId());
break;
case MENU_LOCK_NOTE:
handleLockNote(); // 加锁便签
break;
case MENU_UNLOCK_NOTE:
handleUnlockNote(); // 解锁便签
break;
default:
break;
}
@ -1002,9 +1049,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
// 根据当前状态加载不同的菜单
if (mState == ListEditState.NOTE_LIST) {
getMenuInflater().inflate(R.menu.note_list, menu);
// 根据同步状态设置同步菜单项标题
menu.findItem(R.id.menu_sync).setTitle(
GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
// 注意: Google Sync 功能已移除,同步菜单项已禁用
} else if (mState == ListEditState.SUB_FOLDER) {
getMenuInflater().inflate(R.menu.sub_folder, menu);
} else if (mState == ListEditState.CALL_RECORD_FOLDER) {
@ -1025,16 +1070,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
exportNoteToText(); // 导出笔记为文本
break;
case R.id.menu_sync:
// 同步功能处理
if (isSyncMode()) {
if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
GTaskSyncService.startSync(this); // 开始同步
} else {
GTaskSyncService.cancelSync(this); // 取消同步
}
} else {
startPreferenceActivity(); // 未设置同步账户,跳转到设置
}
// 同步功能已移除,显示提示信息
Toast.makeText(this, R.string.error_sync_not_available, Toast.LENGTH_SHORT).show();
break;
case R.id.menu_setting:
startPreferenceActivity(); // 打开设置
@ -1103,9 +1140,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
/**
*
* : Google Sync , false
*/
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
return false;
}
/**
@ -1172,6 +1210,152 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
/**
* 便
*/
private void handleLockNote() {
if (mFocusNoteDataItem == null) {
return;
}
// 检查是否已设置密码
if (!PasswordManager.isPasswordSet(this)) {
Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show();
return;
}
// 直接加锁,无需密码验证
HashSet<Long> ids = new HashSet<>();
ids.add(mFocusNoteDataItem.getId());
if (DataUtils.batchSetLockStatus(mContentResolver, ids, true)) {
Toast.makeText(this, R.string.toast_lock_success, Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery(); // 刷新列表
} else {
Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
}
/**
* 便
*/
private void handleUnlockNote() {
if (mFocusNoteDataItem == null) {
return;
}
// 检查是否已设置密码
if (!PasswordManager.isPasswordSet(this)) {
Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show();
return;
}
// 显示密码输入对话框
showPasswordDialogForUnlock();
}
/**
* 便
*/
private void showPasswordDialogForUnlock() {
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_unlock_note);
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 (PasswordManager.verifyPassword(NotesListActivity.this, password)) {
HashSet<Long> ids = new HashSet<>();
ids.add(mFocusNoteDataItem.getId());
if (DataUtils.batchSetLockStatus(mContentResolver, ids, false)) {
Toast.makeText(NotesListActivity.this, R.string.toast_unlock_success, Toast.LENGTH_SHORT).show();
startAsyncNotesListQuery(); // 刷新列表
} else {
Toast.makeText(NotesListActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(NotesListActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
* /便
* @param lock truefalse
*/
private void handleBatchLockNotes(boolean lock) {
if (mNotesListAdapter.getSelectedCount() == 0) {
return;
}
// 检查是否已设置密码
if (!PasswordManager.isPasswordSet(this)) {
Toast.makeText(this, R.string.error_no_password_set, Toast.LENGTH_SHORT).show();
return;
}
// 如果是解锁操作,需要验证密码
if (!lock) {
showPasswordDialogForBatchUnlock();
return;
}
// 直接执行批量加锁
HashSet<Long> ids = mNotesListAdapter.getSelectedItemIds();
if (DataUtils.batchSetLockStatus(mContentResolver, ids, true)) {
Toast.makeText(this, R.string.toast_lock_success, Toast.LENGTH_SHORT).show();
mModeCallBack.finishActionMode(); // 结束多选模式
startAsyncNotesListQuery(); // 刷新列表
} else {
Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
}
/**
* 便
*/
private void showPasswordDialogForBatchUnlock() {
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_unlock_note);
builder.setMessage(getString(R.string.message_batch_unlock, mNotesListAdapter.getSelectedCount()));
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 (PasswordManager.verifyPassword(NotesListActivity.this, password)) {
HashSet<Long> ids = mNotesListAdapter.getSelectedItemIds();
if (DataUtils.batchSetLockStatus(mContentResolver, ids, false)) {
Toast.makeText(NotesListActivity.this, R.string.toast_unlock_success, Toast.LENGTH_SHORT).show();
mModeCallBack.finishActionMode(); // 结束多选模式
startAsyncNotesListQuery(); // 刷新列表
} else {
Toast.makeText(NotesListActivity.this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(NotesListActivity.this, R.string.error_wrong_password, Toast.LENGTH_SHORT).show();
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
*
*/
@ -1210,6 +1394,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
// 文件夹长按:显示上下文菜单
mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
} else if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE) {
// 笔记长按:显示上下文菜单(加锁/解锁)
mNotesListView.setOnCreateContextMenuListener(mNoteOnCreateContextMenuListener);
}
}
return false;

@ -27,6 +27,7 @@ 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;
/**
@ -35,6 +36,7 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
*/
public class NotesListItem extends LinearLayout {
private ImageView mAlert; // 提醒图标
private ImageView mLockIcon; // 锁图标
private TextView mTitle; // 标题/内容摘要
private TextView mTime; // 修改时间
private TextView mCallName; // 通话记录联系人姓名
@ -50,6 +52,7 @@ public class NotesListItem extends LinearLayout {
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);
@ -74,6 +77,17 @@ public class NotesListItem extends LinearLayout {
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);
}
// 根据不同数据类型设置不同的显示内容
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
// 通话记录文件夹的特殊显示
@ -141,6 +155,9 @@ public class NotesListItem extends LinearLayout {
// 根据数据类型和位置设置背景
setBackground(data);
// 在设置完所有内容后应用老年人模式,确保字体大小正确
ElderModeUtils.applyElderMode(context, mTitle);
}
/**

@ -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 not use this file except in compliance with 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
@ -16,24 +16,15 @@
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -45,34 +36,15 @@ import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.security.PasswordManager;
import net.micode.notes.account.AccountManager;
import com.google.android.material.appbar.MaterialToolbar;
/**
* Activity
* Google Task
*/
public class NotesPreferenceActivity extends PreferenceActivity {
// 偏好设置文件名
public static final String PREFERENCE_NAME = "notes_preferences";
// 同步账户名称的键11
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_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";
// 同步账户设置的键
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
// 账户授权过滤器的键
private static final String AUTHORITIES_FILTER_KEY = "authorities";
private PreferenceCategory mAccountCategory; // 账户设置类别
private GTaskReceiver mReceiver; // 同步服务广播接收器
private Account[] mOriAccounts; // 原始账户列表(用于检测新添加的账户)
private boolean mHasAddedAccount; // 标记是否添加了新账户
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
@Override
protected void onCreate(Bundle icicle) {
@ -94,8 +66,15 @@ public class NotesPreferenceActivity extends PreferenceActivity {
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 注册同步服务广播
registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); // 注册广播接收器
mOriAccounts = null;
// 添加设置界面的头部视图
MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
android.app.ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
}
}
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
@ -103,38 +82,47 @@ public class NotesPreferenceActivity extends PreferenceActivity {
@Override
protected void onResume() {
super.onResume();
loadSecurityPreference();
loadUserCenterPreference();
}
// 如果用户添加了新账户,需要自动设置同步账户
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
// 检查是否有新账户添加
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean isFound = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
isFound = true;
break;
}
}
// 发现新账户,自动设置为同步账户
if (!isFound) {
setSyncAccount(accountNew.name);
break;
}
}
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);
}
}
refreshUI(); // 刷新界面
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
showSecuritySettingsDialog();
return true;
}
});
}
}
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver); // 注销广播接收器
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));
}
userCenterPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
showUserCenterDialog();
return true;
}
});
}
super.onDestroy();
}
/**
@ -314,37 +302,42 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return !name.isEmpty() && !birthday.isEmpty();
}
/**
*
*/
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() {
private void showUserCenterDialog() {
final boolean isLoggedIn = AccountManager.isUserLoggedIn(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)
};
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.preferences_user_center_title));
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
// 如果不在同步中才能操作账户设置
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// 首次设置账户,显示选择账户对话框
showSelectAccountAlertDialog();
} else {
// 已有账户设置,显示更改账户确认对话框
showChangeAccountConfirmAlertDialog();
public void onClick(DialogInterface dialog, int which) {
if (isLoggedIn) {
if (which == 0) {
handleLogout();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show(); // 同步中不能更改账户
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);
}
}
return true;
}
});
builder.show();
// 添加修改密保问题的选项
Preference securityPref = new Preference(this);
securityPref.setTitle("修改密保问题");
@ -470,294 +463,193 @@ public class NotesPreferenceActivity extends PreferenceActivity {
builder.show();
}
/**
*
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// 设置按钮状态和文字
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this); // 取消同步
}
});
private void handleLogout() {
if (AccountManager.logout(this)) {
Toast.makeText(this, R.string.toast_logout_success, Toast.LENGTH_SHORT).show();
loadUserCenterPreference();
} else {
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this); // 开始同步
}
});
}
// 只有设置了同步账户才能启用同步按钮
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 设置上次同步时间显示
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString()); // 显示同步进度
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
// 格式化显示上次同步时间
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE); // 从未同步过
}
Toast.makeText(this, R.string.error_operation_failed, Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
private void showSecuritySettingsDialog() {
final boolean passwordSet = PasswordManager.isPasswordSet(this);
/**
*
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 自定义对话框标题
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null); // 不显示确定按钮
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts; // 保存当前账户列表
mHasAddedAccount = false; // 重置标记
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
// 填充账户列表
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index; // 标记已选账户
}
items[index++] = account.name;
}
// 设置单选列表
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 选择账户并保存
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI(); // 刷新界面
}
});
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)
};
}
// 添加"添加账户"选项
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() {
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(View v) {
mHasAddedAccount = true; // 标记添加了新账户
// 启动添加账户界面
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls" // 限制为Google账户
});
startActivityForResult(intent, -1);
dialog.dismiss(); // 关闭对话框
public void onClick(DialogInterface dialog, int which) {
if (passwordSet) {
if (which == 0) {
showChangePasswordDialog();
} else if (which == 1) {
showClearPasswordConfirmDialog();
}
} else {
showSetPasswordDialog();
}
}
});
builder.show();
}
/**
*
*/
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() {
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) {
if (which == 0) {
showSelectAccountAlertDialog(); // 显示选择账户对话框
} else if (which == 1) {
removeSyncAccount(); // 移除同步账户
refreshUI(); // 刷新界面
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();
}
// which == 2 取消,不做任何操作
}
});
dialogBuilder.show();
}
/**
* Google
* @return Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google"); // 获取Google类型账户
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
*
* @param account
*/
private void setSyncAccount(String account) {
// 只有当账户变更时才执行操作
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 保存账户名
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 清空账户名
}
editor.commit();
// 清除上次同步时间
setLastSyncTime(this, 0);
// 在新线程中清除本地与GTask相关的同步信息
new Thread(new Runnable() {
@Override
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); // 清空GTask ID
values.put(NoteColumns.SYNC_ID, 0); // 重置同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
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;
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show(); // 显示设置成功提示
}
showOldPasswordVerificationDialog(newPassword);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.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();
// 在新线程中清除本地与GTask相关的同步信息
new Thread(new Runnable() {
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 run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, ""); // 清空GTask ID
values.put(NoteColumns.SYNC_ID, 0); // 重置同步ID
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
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();
}
}
}).start();
}
/**
*
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, ""); // 默认返回空字符串
}
/**
*
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/**
*
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0); // 默认返回0
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();
}
/**
* GTask广
* 广
*/
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));
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();
}
}
}
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 点击返回按钮,返回笔记列表界面
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); // 清理Activity栈
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:

@ -0,0 +1,113 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may 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();
}
}
}

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/on_primary"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/text_secondary"
android:pathData="M12.5,20C17.2,20 21,16.2 21,11.5S17.2,3 12.5,3 4,6.8 4,11.5 7.8,20 12.5,20zM13,13h5v-2h-5V8h-2v3H6v2h5v3h2v-3z" />
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#309760" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#309760" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
</vector>

@ -0,0 +1,96 @@
<?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 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface"
android:elevation="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_login"
android:textSize="24sp"
android:textColor="@color/text_primary"
android:layout_marginBottom="32dp" />
<EditText
android:id="@+id/et_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="text"
android:maxLines="1"
android:layout_marginBottom="16dp"
android:padding="12dp"
android:background="@drawable/edit_white" />
<EditText
android:id="@+id/et_login_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_login_password"
android:inputType="textPassword"
android:maxLines="1"
android:layout_marginBottom="24dp"
android:padding="12dp"
android:background="@drawable/edit_white" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/btn_login"
android:layout_marginBottom="16dp"
android:backgroundTint="@color/primary"
style="?android:attr/buttonBarButtonStyle" />
<Button
android:id="@+id/btn_login_cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
android:backgroundTint="@color/primary"
style="?android:attr/buttonBarButtonStyle" />
<TextView
android:id="@+id/tv_login_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_register_account"
android:textColor="@color/text_link"
android:layout_marginTop="24dp"
android:padding="8dp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,118 @@
<?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 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/background">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface"
android:elevation="4dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_register"
android:textSize="24sp"
android:textColor="@color/text_primary"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="32dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center_vertical">
<EditText
android:id="@+id/et_register_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_username"
android:inputType="text"
android:maxLines="1"
android:layout_marginBottom="16dp"
android:padding="12dp"
android:background="@drawable/edit_white" />
<EditText
android:id="@+id/et_register_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_login_password"
android:inputType="textPassword"
android:maxLines="1"
android:layout_marginBottom="16dp"
android:padding="12dp"
android:background="@drawable/edit_white" />
<EditText
android:id="@+id/et_register_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_confirm_password"
android:inputType="textPassword"
android:maxLines="1"
android:layout_marginBottom="24dp"
android:padding="12dp"
android:background="@drawable/edit_white" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/btn_register_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/cancel"
android:layout_marginEnd="8dp"
android:backgroundTint="@color/primary"
style="?android:attr/buttonBarButtonStyle" />
<Button
android:id="@+id/btn_register"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/btn_register"
android:layout_marginStart="8dp"
android:backgroundTint="@color/primary"
style="?android:attr/buttonBarButtonStyle" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -1,32 +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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dip"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/preferences_add_account" />
</LinearLayout>

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/camera_preview_title"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="center"
android:layout_marginBottom="16dp" />
<ImageView
android:id="@+id/preview_image"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:layout_marginBottom="16dp"
android:contentDescription="@string/camera_preview_image" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<Button
android:id="@+id/btn_retake"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/camera_retake"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btn_confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/camera_confirm"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:cardCornerRadius="20dp"
app:cardElevation="8dp"
app:cardBackgroundColor="@color/surface"
app:strokeColor="@color/outline"
app:strokeWidth="1dp"
app:cardPreventCornerOverlap="true"
app:contentPadding="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="0dp">
<!-- 标题区域 -->
<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/image_insert_title"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center"
android:textColor="@color/text_primary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 图库选项卡片 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/btn_gallery"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@+id/title_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/gallery_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_gallery"
app:tint="@color/primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/gallery_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/image_insert_gallery"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/gallery_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 分隔线 -->
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@color/divider"
app:layout_constraintTop_toBottomOf="@+id/btn_gallery"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 相机选项卡片 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/btn_camera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/camera_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="@color/primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/camera_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/image_insert_camera"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/camera_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 分隔线 -->
<View
android:id="@+id/divider2"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@color/divider"
app:layout_constraintTop_toBottomOf="@+id/btn_camera"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 取消按钮 -->
<TextView
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:padding="16dp"
android:text="@android:string/cancel"
android:textSize="16sp"
android:textColor="@color/primary"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
app:layout_constraintTop_toBottomOf="@+id/divider2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:background="@android:color/white"
android:padding="4dp">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:maxHeight="400dp"
android:scaleType="fitCenter"
android:contentDescription="@string/image_content_description" />
<ImageButton
android:id="@+id/btn_delete"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="@android:drawable/ic_menu_delete"
android:contentDescription="@string/delete_image" />
<TextView
android:id="@+id/tv_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_margin="8dp"
android:background="@android:color/holo_blue_light"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone" />
</FrameLayout>

@ -15,81 +15,90 @@
limitations under the License.
-->
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface"
android:elevation="4dp" />
<LinearLayout
android:id="@+id/note_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/tv_modified_date"
android:layout_width="0dip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="left|center_vertical"
android:layout_marginRight="8dip"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
android:textAppearance="@style/TextAppearanceSecondaryItem"
android:gravity="center_vertical" />
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@drawable/title_alert" />
<TextView
android:id="@+id/tv_alert_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="2dip"
android:layout_marginRight="8dip"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
android:textAppearance="@style/TextAppearanceSecondaryItem"
android:layout_marginStart="2dp"
android:layout_marginEnd="8dp" />
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_btn_set_color" />
<!-- 移除无功能的ImageButton -->
</LinearLayout>
<LinearLayout
android:id="@+id/sv_note_edit"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:layout_width="match_parent"
android:layout_height="7dp"
android:background="@drawable/bg_color_btn_mask" />
<ScrollView
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:layout_gravity="left|top"
android:fadingEdgeLength="0dip">
android:fadingEdgeLength="0dp">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left|top"
android:background="@null"
@ -99,29 +108,41 @@
android:textAppearance="@style/TextAppearancePrimaryItem"
android:lineSpacingMultiplier="1.2" />
<LinearLayout
android:id="@+id/note_attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="16dp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/note_edit_list"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="-10dip"
android:layout_marginLeft="-10dp"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:layout_width="match_parent"
android:layout_height="7dp"
android:background="@drawable/bg_color_btn_mask" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/btn_set_bg_color"
android:layout_height="43dip"
android:layout_width="wrap_content"
android:background="@drawable/bg_color_btn_mask"
android:layout_gravity="top|right" />
android:layout_height="43dp"
android:layout_width="43dp"
android:src="@android:drawable/ic_menu_edit"
app:tint="@color/text_primary"
android:layout_gravity="top|right"
android:layout_marginTop="?attr/actionBarSize"
android:contentDescription="@string/change_background_color"
android:background="?attr/selectableItemBackgroundBorderless" />
<LinearLayout
android:id="@+id/note_bg_color_selector"
@ -397,4 +418,4 @@
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -15,64 +15,116 @@
limitations under the License.
-->
<FrameLayout
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/note_item"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/surface"
app:strokeWidth="0dp"
app:rippleColor="@color/ripple">
<LinearLayout
android:layout_width="fill_parent"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
android:padding="16dp">
<LinearLayout
android:layout_width="0dip"
android:id="@+id/content_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
android:orientation="vertical"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/alert_container"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:visibility="gone" />
android:visibility="gone"
android:layout_marginBottom="4dp" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_lock_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_lock"
android:tint="@color/text_secondary"
android:visibility="gone"
android:layout_marginEnd="4dp" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dip"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true" />
android:singleLine="true"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:layout_marginEnd="8dp" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
android:textAppearance="@style/TextAppearanceSecondaryItem"
android:textColor="@color/text_secondary" />
</LinearLayout>
</LinearLayout>
<CheckBox
android:id="@android:id/checkbox"
<LinearLayout
android:id="@+id/alert_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"/>
</FrameLayout>
android:orientation="vertical"
android:layout_gravity="center_vertical"
android:gravity="center"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:contentDescription="@string/menu_alert"
android:src="@drawable/ic_alarm"
app:tint="@color/text_secondary"
android:visibility="gone" />
<CheckBox
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:visibility="gone"
android:layout_marginStart="8dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

@ -15,44 +15,70 @@
limitations under the License.
-->
<FrameLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bar_bg"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="#FFEAD1AE"
android:textSize="@dimen/text_font_size_medium" />
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/TextAppearanceMedium"
android:textColor="@color/text_primary" />
</com.google.android.material.appbar.MaterialToolbar>
<ListView
android:id="@+id/notes_list"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingTop="8dp"
android:paddingBottom="80dp"
android:clipToPadding="false"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
android:fadingEdge="none"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/btn_new_note"
android:background="@drawable/new_note"
android:layout_width="match_parent"
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_new_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:layout_gravity="bottom" />
</FrameLayout>
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
android:src="@drawable/ic_add"
app:backgroundTint="@color/primary"
app:tint="@color/on_primary"
app:fabSize="normal"
app:elevation="6dp"
app:pressedTranslationZ="12dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -3,7 +3,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 not use this file except in compliance with 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
@ -21,21 +21,11 @@
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:id="@+id/preference_sync_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dip"
android:layout_marginLeft="30dip"
android:layout_marginRight="30dip"
style="?android:attr/textAppearanceMedium"
android:text="@string/preferences_button_sync_immediately"/>
<TextView
android:id="@+id/prefenerece_sync_status_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface"
android:elevation="4dp" />
</LinearLayout>
</LinearLayout>

@ -16,15 +16,25 @@
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
<item
android:id="@+id/menu_insert_image"
android:icon="@android:drawable/ic_menu_camera"
android:title="@string/menu_insert_image"
app:showAsAction="never"
android:orderInCategory="2" />
<item
android:id="@+id/menu_delete"
android:title="@string/menu_delete"/>
android:icon="@android:drawable/ic_menu_delete"
android:title="@string/menu_delete"
app:showAsAction="never"
android:orderInCategory="4" />
<item
android:id="@+id/menu_font_size"

@ -28,4 +28,16 @@
android:title="@string/menu_delete"
android:icon="@drawable/menu_delete"
android:showAsAction="always|withText" />
<item
android:id="@+id/lock"
android:title="@string/menu_lock"
android:icon="@drawable/ic_lock"
android:showAsAction="always|withText" />
<item
android:id="@+id/unlock"
android:title="@string/menu_unlock"
android:icon="@drawable/ic_lock_open"
android:showAsAction="always|withText" />
</menu>

@ -0,0 +1,51 @@
<?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.
-->
<resources>
<!-- Dark Mode Color Overrides -->
<!-- Background Colors -->
<color name="background">#121212</color>
<color name="surface">#1E1E1E</color>
<color name="surface_variant">#2D2D2D</color>
<!-- Text Colors -->
<color name="text_primary">#E0E0E0</color>
<color name="text_secondary">#A0A0A0</color>
<color name="text_hint">#757575</color>
<!-- On Colors -->
<color name="on_background">#E0E0E0</color>
<color name="on_surface">#E0E0E0</color>
<color name="on_surface_variant">#B0B0B0</color>
<!-- Note Background Colors (Dark Mode) -->
<color name="note_bg_yellow">#4A4536</color>
<color name="note_bg_red">#4A3636</color>
<color name="note_bg_blue">#364A5A</color>
<color name="note_bg_green">#364A3E</color>
<color name="note_bg_white">#1E1E1E</color>
<!-- Divider Colors -->
<color name="divider">#2D2D2D</color>
<color name="outline">#404040</color>
<!-- Ripple Effect -->
<color name="ripple">#3352D399</color>
<!-- Status Bar & Navigation Bar -->
<color name="status_bar">#121212</color>
<color name="navigation_bar">#121212</color>
</resources>

@ -1,7 +1,20 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Notesmaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
<!-- Base application theme for dark mode. -->
<style name="Base.Theme.NotesMaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Override light status bar and navigation bar for dark mode -->
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
<!-- Ensure proper contrast for dark mode -->
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="android:textColorHint">@color/text_hint</item>
</style>
<!-- NoteTheme dark mode overrides (inherits from Theme.NotesMaster) -->
<style name="NoteTheme" parent="Theme.NotesMaster">
<!-- Override for dark mode -->
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
</style>
</resources>

@ -17,4 +17,57 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<!-- Material Design 3 Color System -->
<!-- Primary Colors -->
<color name="primary">#34D399</color>
<color name="primary_container">#DCFCE7</color>
<color name="on_primary">#FFFFFF</color>
<color name="on_primary_container">#064E3B</color>
<!-- Secondary Colors -->
<color name="secondary">#6B7280</color>
<color name="secondary_container">#E5E7EB</color>
<color name="on_secondary">#FFFFFF</color>
<color name="on_secondary_container">#1F2937</color>
<!-- Background Colors -->
<color name="background">#FAFAFA</color>
<color name="surface">#FFFFFF</color>
<color name="surface_variant">#F3F4F6</color>
<color name="on_background">#1F2937</color>
<color name="on_surface">#1F2937</color>
<color name="on_surface_variant">#4B5563</color>
<!-- Note Background Colors (Modern Soft Tones) -->
<color name="note_bg_yellow">#FEF3C7</color>
<color name="note_bg_red">#FEE2E2</color>
<color name="note_bg_blue">#DBEAFE</color>
<color name="note_bg_green">#DCFCE7</color>
<color name="note_bg_white">#FFFFFF</color>
<!-- Text Colors -->
<color name="text_primary">#1F2937</color>
<color name="text_secondary">#6B7280</color>
<color name="text_link">#2563EB</color>
<color name="text_hint">#9CA3AF</color>
<!-- Accent Colors -->
<color name="accent">#34D399</color>
<color name="accent_container">#DCFCE7</color>
<!-- Error Colors -->
<color name="error">#EF4444</color>
<color name="error_container">#FEE2E2</color>
<color name="on_error">#FFFFFF</color>
<!-- Divider Colors -->
<color name="divider">#E5E7EB</color>
<color name="outline">#D1D5DB</color>
<!-- Ripple Effect -->
<color name="ripple">#1A34D399</color>
<!-- Legacy Colors (for backward compatibility) -->
<!-- Note: primary_text_dark and secondary_text_dark are defined in res/color/ directory as selectors -->
</resources>

@ -64,6 +64,21 @@
<string name="menu_folder_change_name">Change folder name</string>
<string name="folder_exist">The folder %1$s exist, please rename</string>
<string name="menu_share">Share</string>
<string name="menu_insert_image">Insert image</string>
<string name="image_insert_title">Insert image</string>
<string name="image_insert_gallery">Choose from gallery</string>
<string name="image_insert_camera">Take a photo</string>
<string name="image_content_description">Attachment image</string>
<string name="delete_image">Delete image</string>
<string name="confirm_delete_image">Are you sure you want to delete this image?</string>
<string name="permission_denied">Permission denied, cannot access image</string>
<string name="image_added">Image added successfully</string>
<string name="image_deleted">Image deleted</string>
<string name="failed_to_add_image">Failed to add image</string>
<string name="camera_preview_title">Preview Photo</string>
<string name="camera_preview_image">Preview image</string>
<string name="camera_retake">Retake</string>
<string name="camera_confirm">Confirm</string>
<string name="menu_send_to_desktop">Send to home</string>
<string name="menu_alert">Remind me</string>
<string name="menu_remove_remind">Delete reminder</string>
@ -84,44 +99,39 @@
<string name="success_sdcard_export">Export successful</string>
<string name="failed_sdcard_export">Export fail</string>
<string name="format_exported_file_location">Export text file (%1$s) to SD (%2$s) directory</string>
<!-- Sync -->
<string name="ticker_syncing">Syncing notes...</string>
<string name="ticker_success">Sync is successful</string>
<string name="ticker_fail">Sync is failed</string>
<string name="ticker_cancel">Sync is canceled</string>
<string name="success_sync_account">Sync is successful with account %1$s</string>
<string name="error_sync_network">Sync failed, please check network and account settings</string>
<string name="error_sync_internal">Sync failed, internal error occurs</string>
<string name="error_sync_cancelled">Sync is canceled</string>
<string name="sync_progress_login">Logging into %1$s...</string>
<string name="sync_progress_init_list">Getting remote note list...</string>
<string name="sync_progress_syncing">Synchronize local notes with Google Task...</string>
<!-- Preferences -->
<string name="preferences_title">Settings</string>
<string name="preferences_account_title">Sync account</string>
<string name="preferences_account_summary">Sync notes with google task</string>
<string name="preferences_last_sync_time">Last sync time %1$s</string>
<string name="preferences_last_sync_time_format">yyyy-MM-dd hh:mm:ss</string>
<string name="preferences_add_account">Add account</string>
<string name="preferences_menu_change_account">Change sync account</string>
<string name="preferences_menu_remove_account">Remove sync account</string>
<string name="preferences_menu_cancel">Cancel</string>
<string name="preferences_button_sync_immediately">Sync immediately</string>
<string name="preferences_button_sync_cancel">Cancel syncing</string>
<string name="preferences_dialog_change_account_title">Current account %1$s</string>
<string name="preferences_dialog_change_account_warn_msg">All sync related information will be deleted, which may result in duplicated items sometime</string>
<string name="preferences_dialog_select_account_title">Sync notes</string>
<string name="preferences_dialog_select_account_tips">Please select a google account. Local notes will be synced with google task.</string>
<string name="preferences_toast_cannot_change_account">Cannot change the account because sync is in progress</string>
<string name="preferences_toast_success_set_accout">%1$s has been set as the sync account</string>
<string name="preferences_user_center_title">User Center</string>
<string name="preferences_user_center_summary">Current user: %1$s</string>
<string name="preferences_user_center_not_logged_in">Not logged in</string>
<string name="menu_login">Login</string>
<string name="menu_register">Register</string>
<string name="menu_logout">Logout</string>
<string name="toast_logout_success">Logout successful</string>
<string name="preferences_bg_random_appear_title">New note background color random</string>
<string name="preferences_elder_mode_title">Elderly Mode</string>
<string name="preferences_elder_mode_summary">Use larger font size for better readability</string>
<string name="preferences_elder_mode_title">Elder mode</string>
<string name="button_delete">Delete</string>
<string name="call_record_folder_name">Call notes</string>
<string name="hint_foler_name">Input name</string>
<!-- 用户登录/注册相关字符串 -->
<string name="title_login">Login</string>
<string name="title_register">Register</string>
<string name="btn_login">Login</string>
<string name="btn_register">Register</string>
<string name="text_register_account">Don\'t have an account? Register</string>
<string name="hint_username">Username</string>
<string name="hint_login_password">Password</string>
<string name="hint_confirm_password">Confirm Password</string>
<string name="error_username_empty">Username cannot be empty</string>
<string name="error_login_failed">Username or password is incorrect</string>
<string name="error_username_exists">Username already exists</string>
<string name="error_register_failed">Registration failed</string>
<string name="toast_login_success">Login successful</string>
<string name="toast_register_success">Registration successful</string>
<string name="error_sync_not_available">Sync feature is no longer available</string>
<string name="search_label">Searching Notes</string>
<string name="search_hint">Search notes</string>
<string name="search_setting_description">Text in your notes</string>
@ -134,4 +144,40 @@
<item quantity="other"><xliff:g id="number" example="15">%1$s</xliff:g> results for \"<xliff:g id="search" example="???">%2$s</xliff:g>\"</item>
</plurals>
<string name="change_background_color">Change background color</string>
<!-- 便签加锁相关字符串 -->
<string name="menu_lock">加锁</string>
<string name="menu_unlock">解锁</string>
<string name="menu_security_settings">加密设置</string>
<string name="menu_set_password">设置密码</string>
<string name="menu_change_password">修改密码</string>
<string name="menu_clear_password">清除密码</string>
<string name="preferences_security_category">安全设置</string>
<string name="preferences_security_title">加密设置</string>
<string name="preferences_security_summary">设置、修改或清除便签密码</string>
<string name="preferences_password_set">密码已设置</string>
<string name="preferences_password_not_set">密码未设置</string>
<string name="title_locked_note">便签已加锁</string>
<string name="title_unlock_note">解锁便签</string>
<string name="title_set_password">设置密码</string>
<string name="title_change_password">修改密码</string>
<string name="title_verify_password">验证密码</string>
<string name="hint_enter_password">请输入密码</string>
<string name="hint_password">密码</string>
<string name="hint_new_password">新密码</string>
<string name="hint_old_password">原密码</string>
<string name="error_wrong_password">密码错误</string>
<string name="error_no_password_set">请先设置密码</string>
<string name="error_password_empty">密码不能为空</string>
<string name="error_password_mismatch">两次密码不一致</string>
<string name="error_operation_failed">操作失败</string>
<string name="toast_lock_success">加锁成功</string>
<string name="toast_unlock_success">解锁成功</string>
<string name="toast_password_set_success">密码设置成功</string>
<string name="toast_password_change_success">密码修改成功</string>
<string name="toast_password_cleared">密码已清除</string>
<string name="message_batch_unlock">解锁选中的 %d 条便签</string>
<string name="message_clear_password_confirm">确定要清除密码吗?清除后所有便签将不再受保护。</string>
</resources>

@ -15,54 +15,119 @@
limitations under the License.
-->
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="TextAppearanceSuper">
<item name="android:textSize">@dimen/text_font_size_super</item>
<item name="android:textColorLink">#0000ff</item>
<item name="android:textColorLink">@color/text_link</item>
<item name="android:lineSpacingMultiplier">1.4</item>
</style>
<style name="TextAppearanceLarge">
<item name="android:textSize">@dimen/text_font_size_large</item>
<item name="android:textColorLink">#0000ff</item>
<item name="android:textColorLink">@color/text_link</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="TextAppearanceMedium">
<item name="android:textSize">@dimen/text_font_size_medium</item>
<item name="android:textColorLink">#0000ff</item>
<item name="android:textColorLink">@color/text_link</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="TextAppearanceNormal">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColorLink">#0000ff</item>
<item name="android:textColorLink">@color/text_link</item>
<item name="android:lineSpacingMultiplier">1.4</item>
</style>
<style name="TextAppearancePrimaryItem">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_primary</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="TextAppearanceSecondaryItem">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_secondary</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="TextAppearanceUnderMenuIcon">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="HighlightTextAppearancePrimary">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="HighlightTextAppearanceSecondary">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_secondary</item>
</style>
<style name="NoteTheme" parent="@android:style/Theme.Holo.Light">
<!-- Material Design 3 Styles -->
<style name="NoteTheme" parent="Theme.NotesMaster">
<!-- Inherits all configuration from Theme.NotesMaster -->
<!-- Additional dialog-specific styling if needed -->
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
</style>
<style name="NoteActionBarStyle" parent="Theme.Material3.DayNight">
<item name="android:background">@color/surface</item>
<item name="android:elevation">4dp</item>
</style>
<!-- Material Card Style -->
<style name="MaterialCard" parent="Widget.Material3.CardView.Elevated">
<item name="cardCornerRadius">12dp</item>
<item name="cardElevation">2dp</item>
<item name="cardBackgroundColor">@color/surface</item>
<item name="contentPadding">16dp</item>
</style>
<!-- Material Button Styles -->
<style name="MaterialButton" parent="Widget.Material3.Button">
<item name="android:textAllCaps">false</item>
<item name="cornerRadius">8dp</item>
<item name="android:paddingStart">24dp</item>
<item name="android:paddingEnd">24dp</item>
<item name="android:paddingTop">12dp</item>
<item name="android:paddingBottom">12dp</item>
</style>
<style name="MaterialButtonOutlined" parent="Widget.Material3.Button.OutlinedButton">
<item name="android:textAllCaps">false</item>
<item name="cornerRadius">8dp</item>
<item name="strokeColor">@color/outline</item>
<item name="strokeWidth">1dp</item>
<item name="android:paddingStart">24dp</item>
<item name="android:paddingEnd">24dp</item>
<item name="android:paddingTop">12dp</item>
<item name="android:paddingBottom">12dp</item>
</style>
<!-- Material FAB Style -->
<style name="MaterialFAB" parent="Widget.Material3.FloatingActionButton.Primary">
<item name="fabSize">normal</item>
<item name="backgroundTint">@color/primary</item>
<item name="tint">@color/on_primary</item>
</style>
<!-- Material EditText Style -->
<style name="MaterialEditText" parent="Widget.Material3.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/outline</item>
<item name="boxCornerRadiusBottomEnd">8dp</item>
<item name="boxCornerRadiusBottomStart">8dp</item>
<item name="boxCornerRadiusTopEnd">8dp</item>
<item name="boxCornerRadiusTopStart">8dp</item>
</style>
<!-- Legacy Theme (for backward compatibility) -->
<style name="NoteThemeLegacy" parent="@android:style/Theme.Holo.Light">
<item name="android:actionBarStyle">@style/NoteActionBarStyle</item>
</style>
<style name="NoteActionBarStyle" parent="@android:style/Widget.Holo.Light.ActionBar.Solid">
<style name="NoteActionBarStyleLegacy" parent="@android:style/Widget.Holo.Light.ActionBar.Solid">
<item name="android:visibility">visible</item>
</style>
</resources>

@ -1,9 +1,45 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Notesmaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<!-- Base application theme with Material Design 3 -->
<style name="Base.Theme.NotesMaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Primary Colors -->
<item name="colorPrimary">@color/primary</item>
<item name="colorOnPrimary">@color/on_primary</item>
<item name="colorPrimaryContainer">@color/primary_container</item>
<item name="colorOnPrimaryContainer">@color/on_primary_container</item>
<!-- Secondary Colors -->
<item name="colorSecondary">@color/secondary</item>
<item name="colorOnSecondary">@color/on_secondary</item>
<item name="colorSecondaryContainer">@color/secondary_container</item>
<item name="colorOnSecondaryContainer">@color/on_secondary_container</item>
<!-- Background & Surface Colors -->
<item name="android:windowBackground">@color/background</item>
<item name="android:colorBackground">@color/background</item>
<item name="colorSurface">@color/surface</item>
<item name="colorOnSurface">@color/on_surface</item>
<item name="colorOnSurfaceVariant">@color/on_surface_variant</item>
<!-- Error Colors -->
<item name="colorError">@color/error</item>
<item name="colorOnError">@color/on_error</item>
<item name="colorErrorContainer">@color/error_container</item>
<item name="colorOnErrorContainer">@color/on_error</item>
<!-- Text Colors -->
<item name="android:textColorPrimary">@color/text_primary</item>
<item name="android:textColorSecondary">@color/text_secondary</item>
<item name="android:textColorHint">@color/text_hint</item>
<!-- Ripple Effect -->
<item name="android:colorControlHighlight">@color/ripple</item>
<!-- Status Bar & Navigation Bar -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />
<style name="Theme.NotesMaster" parent="Base.Theme.NotesMaster" />
</resources>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path name="external_files" path="." />
<files-path name="files" path="." />
<cache-path name="cache" path="." />
<external-path name="external" path="." />
</paths>

@ -17,8 +17,10 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_sync_account_key">
<PreferenceCategory android:title="@string/preferences_user_center_title">
<Preference
android:key="pref_user_center"
android:title="@string/preferences_user_center_title" />
</PreferenceCategory>
<PreferenceCategory>
@ -29,7 +31,12 @@
<CheckBoxPreference
android:key="pref_key_elder_mode"
android:title="@string/preferences_elder_mode_title"
android:summary="@string/preferences_elder_mode_summary"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/preferences_security_category">
<Preference
android:key="pref_security_settings"
android:title="@string/preferences_security_title" />
</PreferenceCategory>
</PreferenceScreen>

Loading…
Cancel
Save