Compare commits

..

27 Commits

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

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

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/main.iml" filepath="$PROJECT_DIR$/main.iml" />
</modules>
</component>
</project>

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

@ -2,9 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<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.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission
android:name="android.permission.MANAGE_ACCOUNTS"
@ -13,6 +16,13 @@
<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.SET_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 修复application标签确保格式正确属性完整 -->
<application
@ -113,26 +123,67 @@
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" />
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.TIME_CHANGED" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
<action android:name="net.micode.notes.ALARM_TRIGGERED" />
</intent-filter>
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar"
android:exported="false"
android:showOnLockScreen="true"
android:turnScreenOn="true"
android:keepScreenOn="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize"
tools:replace="android:theme" />
<activity
android:name=".ui.NewAlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Dialog"
android:exported="false"
android:showOnLockScreen="true"
android:turnScreenOn="true"
android:keepScreenOn="true"
android:excludeFromRecents="true"
android:noHistory="true"
android:screenOrientation="portrait"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light"
android:exported="false"
tools:replace="android:theme" />
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" />
<service
android:name=".ui.AlarmAlertService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".ui.NewAlarmAlertService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />

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

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

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/main.iml" filepath="$PROJECT_DIR$/main.iml" />
</modules>
</component>
</project>

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

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -29,11 +29,13 @@ public class Notes {
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
* {@link Notes#ID_TODO_FOLDER} is to store todo items
*/
public static final int ID_ROOT_FOLDER = 0;
public static final int ID_TEMPARAY_FOLDER = -1;
public static final int ID_CALL_RECORD_FOLDER = -2;
public static final int ID_TRASH_FOLER = -3;
public static final int ID_TRASH_FOLDER = -3;
public static final int ID_TODO_FOLDER = -4;
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
@ -86,13 +88,24 @@ public class Notes {
*/
public static final String MODIFIED_DATE = "modified_date";
/**
* Alert date
* <P> Type: INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* Delete date
* <P> Type: INTEGER (long) </P>
*/
public static final String DELETED_DATE = "deleted_date";
/**
* Delete flag
* <P> Type: INTEGER (0 or 1) </P>
*/
public static final String DELETED = "deleted";
/**
* Folder's name or text content of note
* <P> Type: TEXT </P>
@ -165,6 +178,24 @@ public class Notes {
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
/**
* Whether the note is pinned to top
* <P> Type: INTEGER (0: not pinned, 1: pinned) </P>
*/
public static final String PINNED = "pinned";
/**
* Whether the note is encrypted
* <P> Type: INTEGER (0: not encrypted, 1: encrypted) </P>
*/
public static final String ENCRYPTED = "encrypted";
/**
* Encrypted password hash
* <P> Type: TEXT </P>
*/
public static final String PASSWORD_HASH = "password_hash";
}
public interface DataColumns {

@ -30,7 +30,7 @@ import net.micode.notes.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 6;
public interface TABLE {
public static final String NOTE = "note";
@ -49,6 +49,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.DELETED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.DELETED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
@ -60,7 +62,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ENCRYPTED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.PASSWORD_HASH + " TEXT NOT NULL DEFAULT ''" +
")";
private static final String CREATE_DATA_TABLE_SQL =
@ -199,10 +204,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLDER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLDER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
@ -265,7 +270,15 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* create trash folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create todo folder which is used for todo items
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TODO_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
@ -287,7 +300,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
@ -321,6 +334,16 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
upgradeToV4(db);
oldVersion++;
}
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
if (oldVersion == 5) {
upgradeToV6(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
@ -350,7 +373,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
+ " TEXT NOT NULL DEFAULT ''");
// add a trash system folder
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
@ -359,4 +382,25 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.ENCRYPTED
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PASSWORD_HASH
+ " TEXT NOT NULL DEFAULT ''");
}
private void upgradeToV6(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.DELETED
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.DELETED_DATE
+ " INTEGER NOT NULL DEFAULT 0");
// Add todo folder
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TODO_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
}

@ -33,6 +33,7 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.ui.NoteItemData;
public class NotesProvider extends ContentProvider {
@ -49,6 +50,9 @@ public class NotesProvider extends ContentProvider {
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
private static final int URI_LIST_SEARCH = 7;
private static final int URI_TIME_SEARCH = 8;
private static final int URI_DELETED_SEARCH = 9;
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@ -57,6 +61,9 @@ public class NotesProvider extends ContentProvider {
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, "note/list_search", URI_LIST_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, "note/time_search", URI_TIME_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, "note/deleted_search", URI_DELETED_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);
}
@ -67,7 +74,7 @@ public class NotesProvider extends ContentProvider {
*/
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "CASE WHEN data." + DataColumns.DATA3 + " IS NOT NULL AND data." + DataColumns.DATA3 + " != '' THEN TRIM(REPLACE(data." + DataColumns.DATA3 + ", x'0A','')) ELSE TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) END AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
@ -75,8 +82,73 @@ public class NotesProvider extends ContentProvider {
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " LEFT JOIN " + TABLE.DATA + " ON " + TABLE.NOTE + "." + NoteColumns.ID + "=" + TABLE.DATA + "." + DataColumns.NOTE_ID + " AND " + TABLE.DATA + "." + DataColumns.MIME_TYPE + "='" + Notes.DataConstants.NOTE + "'"
+ " WHERE (" + NoteColumns.SNIPPET + " LIKE ? OR data." + DataColumns.DATA3 + " LIKE ?)"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER
+ " AND " + NoteColumns.DELETED + "=0"
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
// 用于 NotesListActivity 的搜索查询
private static String NOTES_LIST_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + ","
+ TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + ","
+ TABLE.NOTE + "." + NoteColumns.CREATED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + ","
+ TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + ","
+ TABLE.NOTE + "." + NoteColumns.PARENT_ID + ","
+ TABLE.NOTE + "." + NoteColumns.SNIPPET + ","
+ TABLE.NOTE + "." + NoteColumns.TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_ID + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.PINNED + ","
+ TABLE.NOTE + "." + NoteColumns.ENCRYPTED
+ " FROM " + TABLE.NOTE
+ " LEFT JOIN " + TABLE.DATA + " ON " + TABLE.NOTE + "." + NoteColumns.ID + "=" + TABLE.DATA + "." + DataColumns.NOTE_ID + " AND " + TABLE.DATA + "." + DataColumns.MIME_TYPE + "='" + Notes.DataConstants.NOTE + "'"
+ " WHERE (" + NoteColumns.SNIPPET + " LIKE ? OR data." + DataColumns.DATA3 + " LIKE ?)"
+ " AND " + NoteColumns.PARENT_ID + "=?"
+ " AND " + NoteColumns.DELETED + "=0"
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
// 用于 NotesListActivity 的时间搜索查询
private static String NOTES_TIME_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + ","
+ TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + ","
+ TABLE.NOTE + "." + NoteColumns.CREATED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + ","
+ TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + ","
+ TABLE.NOTE + "." + NoteColumns.PARENT_ID + ","
+ TABLE.NOTE + "." + NoteColumns.SNIPPET + ","
+ TABLE.NOTE + "." + NoteColumns.TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_ID + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.PINNED + ","
+ TABLE.NOTE + "." + NoteColumns.ENCRYPTED
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.CREATED_DATE + " BETWEEN ? AND ?"
+ " AND " + NoteColumns.PARENT_ID + "=?"
+ " AND " + NoteColumns.DELETED + "=0";
// 用于 NotesListActivity 的回收站搜索查询
private static String NOTES_DELETED_SEARCH_QUERY = "SELECT " + TABLE.NOTE + "." + NoteColumns.ID + ","
+ TABLE.NOTE + "." + NoteColumns.ALERTED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.BG_COLOR_ID + ","
+ TABLE.NOTE + "." + NoteColumns.CREATED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.HAS_ATTACHMENT + ","
+ TABLE.NOTE + "." + NoteColumns.MODIFIED_DATE + ","
+ TABLE.NOTE + "." + NoteColumns.NOTES_COUNT + ","
+ TABLE.NOTE + "." + NoteColumns.PARENT_ID + ","
+ TABLE.NOTE + "." + NoteColumns.SNIPPET + ","
+ TABLE.NOTE + "." + NoteColumns.TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_ID + ","
+ TABLE.NOTE + "." + NoteColumns.WIDGET_TYPE + ","
+ TABLE.NOTE + "." + NoteColumns.PINNED + ","
+ TABLE.NOTE + "." + NoteColumns.ENCRYPTED
+ " FROM " + TABLE.NOTE
+ " LEFT JOIN " + TABLE.DATA + " ON " + TABLE.NOTE + "." + NoteColumns.ID + "=" + TABLE.DATA + "." + DataColumns.NOTE_ID + " AND " + TABLE.DATA + "." + DataColumns.MIME_TYPE + "='" + Notes.DataConstants.NOTE + "'"
+ " WHERE (" + NoteColumns.SNIPPET + " LIKE ? OR data." + DataColumns.DATA3 + " LIKE ?)"
+ " AND " + NoteColumns.DELETED + "=1"
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
@Override
@ -93,7 +165,19 @@ public class NotesProvider extends ContentProvider {
String id = null;
switch (mMatcher.match(uri)) {
case URI_NOTE:
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
// 根据查询条件决定是否过滤已删除的笔记
String noteSelection;
if (selection != null && selection.equals(NoteColumns.DELETED + "=1")) {
// 如果查询条件就是DELETED=1直接使用
noteSelection = selection;
} else if (selection != null && selection.contains(NoteColumns.DELETED + "=1")) {
// 如果查询条件中包含DELETED=1直接使用
noteSelection = selection;
} else {
// 默认过滤掉已删除的笔记将DELETED=0作为基础条件然后添加其他查询条件
noteSelection = NoteColumns.DELETED + "=0" + parseSelection(selection);
}
c = db.query(TABLE.NOTE, projection, noteSelection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
@ -133,7 +217,69 @@ public class NotesProvider extends ContentProvider {
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
new String[] { searchString, searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
case URI_LIST_SEARCH:
// 处理 NotesListActivity 的搜索请求
if (projection == null) {
projection = NoteItemData.PROJECTION;
}
String listSearchString = uri.getQueryParameter("pattern");
String folderId = uri.getQueryParameter("folder_id");
if (TextUtils.isEmpty(listSearchString) || TextUtils.isEmpty(folderId)) {
return null;
}
try {
listSearchString = String.format("%%%s%%", listSearchString);
c = db.rawQuery(NOTES_LIST_SEARCH_QUERY,
new String[] { listSearchString, listSearchString, folderId });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
case URI_DELETED_SEARCH:
// 处理 NotesListActivity 的回收站搜索请求
if (projection == null) {
projection = NoteItemData.PROJECTION;
}
String deletedSearchString = uri.getQueryParameter("pattern");
if (TextUtils.isEmpty(deletedSearchString)) {
return null;
}
try {
deletedSearchString = String.format("%%%s%%", deletedSearchString);
c = db.rawQuery(NOTES_DELETED_SEARCH_QUERY,
new String[] { deletedSearchString, deletedSearchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
case URI_TIME_SEARCH:
// 处理 NotesListActivity 的时间搜索请求
if (projection == null) {
projection = NoteItemData.PROJECTION;
}
String startTime = uri.getQueryParameter("start_time");
String endTime = uri.getQueryParameter("end_time");
String timeFolderId = uri.getQueryParameter("folder_id");
if (TextUtils.isEmpty(startTime) || TextUtils.isEmpty(endTime) || TextUtils.isEmpty(timeFolderId)) {
return null;
}
try {
c = db.rawQuery(NOTES_TIME_SEARCH_QUERY,
new String[] { startTime, endTime, timeFolderId });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
@ -190,7 +336,11 @@ public class NotesProvider extends ContentProvider {
switch (mMatcher.match(uri)) {
case URI_NOTE:
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
// 软删除设置DELETED标志和删除时间
ContentValues values = new ContentValues();
values.put(NoteColumns.DELETED, 1);
values.put(NoteColumns.DELETED_DATE, System.currentTimeMillis());
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
@ -202,7 +352,11 @@ public class NotesProvider extends ContentProvider {
if (noteId <= 0) {
break;
}
count = db.delete(TABLE.NOTE,
// 软删除设置DELETED标志和删除时间
ContentValues itemValues = new ContentValues();
itemValues.put(NoteColumns.DELETED, 1);
itemValues.put(NoteColumns.DELETED_DATE, System.currentTimeMillis());
count = db.update(TABLE.NOTE, itemValues,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
case URI_DATA:

@ -263,7 +263,7 @@ public class GTaskManager {
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id=?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLDER)
}, null);
if (c != null) {
while (c.moveToNext()) {
@ -293,7 +293,7 @@ public class GTaskManager {
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)
String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
@ -428,7 +428,7 @@ public class GTaskManager {
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type=? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)
String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLDER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {
@ -761,7 +761,7 @@ public class GTaskManager {
try {
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, SqlNote.PROJECTION_NOTE,
"(type<>? AND parent_id<>?)", new String[] {
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLER)
String.valueOf(Notes.TYPE_SYSTEM), String.valueOf(Notes.ID_TRASH_FOLDER)
}, NoteColumns.TYPE + " DESC");
if (c != null) {
while (c.moveToNext()) {

@ -29,39 +29,56 @@ import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.EncryptionUtils;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
/**
* WorkingNote 便
* 便
*/
public class WorkingNote {
// Note for the working note
/** 便签对象,用于存储便签的详细数据 */
private Note mNote;
// Note Id
/** 便签 ID */
private long mNoteId;
// Note content
/** 便签内容 */
private String mContent;
// Note mode
/** 便签标题 */
private String mTitle;
/** 便签模式(普通模式或 checklist 模式) */
private int mMode;
/** 提醒日期 */
private long mAlertDate;
/** 修改日期 */
private long mModifiedDate;
/** 背景颜色 ID */
private int mBgColorId;
/** 小组件 ID */
private int mWidgetId;
/** 小组件类型 */
private int mWidgetType;
/** 是否置顶 */
private boolean mPinned;
/** 是否加密 */
private boolean mEncrypted;
/** 密码哈希值 */
private String mPasswordHash;
/** 文件夹 ID */
private long mFolderId;
/** 上下文对象 */
private Context mContext;
/** 日志标签 */
private static final String TAG = "WorkingNote";
/** 是否已删除 */
private boolean mIsDeleted;
/** 便签设置变更监听器 */
private NoteSettingChangedListener mNoteSettingStatusListener;
/**
* 便
* IDMIME DATA1-DATA4
*/
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
@ -72,139 +89,216 @@ public class WorkingNote {
DataColumns.DATA4,
};
/**
* 便便
* ID
*/
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE
NoteColumns.MODIFIED_DATE,
NoteColumns.PINNED,
NoteColumns.ENCRYPTED,
NoteColumns.PASSWORD_HASH
};
/** 数据列索引ID */
private static final int DATA_ID_COLUMN = 0;
/** 数据列索引:内容 */
private static final int DATA_CONTENT_COLUMN = 1;
/** 数据列索引MIME 类型 */
private static final int DATA_MIME_TYPE_COLUMN = 2;
/** 数据列索引:模式 */
private static final int DATA_MODE_COLUMN = 3;
/** 便签列索引:父文件夹 ID */
private static final int NOTE_PARENT_ID_COLUMN = 0;
/** 便签列索引:提醒日期 */
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
/** 便签列索引:背景颜色 ID */
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
/** 便签列索引:小组件 ID */
private static final int NOTE_WIDGET_ID_COLUMN = 3;
/** 便签列索引:小组件类型 */
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
/** 便签列索引:修改日期 */
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
// New note construct
/** 便签列索引:是否置顶 */
private static final int NOTE_PINNED_COLUMN = 6;
/** 便签列索引:是否加密 */
private static final int NOTE_ENCRYPTED_COLUMN = 7;
/** 便签列索引:密码哈希值 */
private static final int NOTE_PASSWORD_HASH_COLUMN = 8;
/**
* 便
* @param context
* @param folderId ID
*/
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
// Existing note construct
mContext = context; // 初始化上下文对象
mAlertDate = 0; // 初始化提醒日期为 0
mModifiedDate = System.currentTimeMillis(); // 设置修改日期为当前时间
mFolderId = folderId; // 设置文件夹 ID
mNote = new Note(); // 创建新的 Note 对象
mNoteId = 0; // 新便签 ID 为 0
mIsDeleted = false; // 初始状态为未删除
mMode = 0; // 初始模式为普通模式
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 初始小组件类型为无效
mPinned = false; // 初始状态为未置顶
mEncrypted = false; // 初始状态为未加密
mPasswordHash = ""; // 初始密码哈希值为空
mTitle = ""; // 初始标题为空
}
/**
* 便
* @param context
* @param noteId 便 ID
* @param folderId ID
*/
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
loadNote();
mContext = context; // 初始化上下文对象
mNoteId = noteId; // 设置便签 ID
mFolderId = folderId; // 设置文件夹 ID
mIsDeleted = false; // 初始状态为未删除
mNote = new Note(); // 创建新的 Note 对象
boolean loaded = loadNote(); // 加载便签数据
if (!loaded) {
Log.e(TAG, "Failed to load note with id:" + noteId); // 加载失败时记录错误日志
}
}
private void loadNote() {
/**
* 便
* 便 ID
* @return
*/
private boolean loadNote() {
// 查询便签基本信息
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
boolean success = false; // 初始化加载成功标志
if (cursor != null) {
if (cursor.moveToFirst()) {
// 读取便签基本属性
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
mPinned = (cursor.getInt(NOTE_PINNED_COLUMN) > 0) ? true : false;
mEncrypted = (cursor.getInt(NOTE_ENCRYPTED_COLUMN) > 0) ? true : false;
mPasswordHash = cursor.getString(NOTE_PASSWORD_HASH_COLUMN);
success = true; // 加载成功
}
cursor.close();
cursor.close(); // 关闭游标
} else {
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
Log.e(TAG, "No note with id:" + mNoteId); // 记录错误日志
}
if (success) {
return loadNoteData(); // 加载成功后加载便签详细数据
}
loadNoteData();
return false; // 加载失败
}
private void loadNoteData() {
/**
* 便
* 便
* @return
*/
private boolean loadNoteData() {
// 查询便签详细数据
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
}, null);
boolean success = false; // 初始化加载成功标志
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
// 处理普通便签数据
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mTitle = cursor.getString(5); // DATA3 列存储标题(索引 5
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
success = true; // 加载成功
} else if (DataConstants.CALL_NOTE.equals(type)) {
// 处理通话便签数据
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
success = true; // 加载成功
} else {
Log.d(TAG, "Wrong note type with type:" + type);
Log.d(TAG, "Wrong note type with type:" + type); // 记录错误类型日志
}
} while (cursor.moveToNext());
}
cursor.close();
cursor.close(); // 关闭游标
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
Log.e(TAG, "No data with id:" + mNoteId); // 记录错误日志
}
return success; // 返回加载结果
}
/**
* 便
* @param context
* @param folderId ID
* @param widgetId ID
* @param widgetType
* @param defaultBgColorId ID
* @return 便
*/
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
note.setBgColorId(defaultBgColorId); // 设置默认背景颜色
note.setWidgetId(widgetId); // 设置小组件 ID
note.setWidgetType(widgetType); // 设置小组件类型
return note;
}
/**
* ID 便
* @param context
* @param id 便 ID
* @return 便
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
* 便
* @return
*/
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
if (isWorthSaving()) { // 检查是否值得保存
if (!existInDatabase()) { // 检查是否已存在于数据库
// 创建新便签
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
mNote.syncNote(mContext, mNoteId);
mNote.syncNote(mContext, mNoteId); // 同步便签数据到数据库
/**
* Update widget content if there exist any widget of this note
* 便
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
mNoteSettingStatusListener.onWidgetChanged(); // 通知小组件变更
}
return true;
} else {
@ -212,156 +306,341 @@ public class WorkingNote {
}
}
/**
* 便
* @return
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
* 便
* @return
*/
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
// 已删除、新建且内容为空、已存在且未修改的便签不值得保存
return false;
} else {
return true;
}
}
/**
* 便
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
*
* @param date
* @param set
*/
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
Log.d(TAG, "setAlertDate: date=" + date + ", set=" + set);
mAlertDate = date; // 设置提醒日期
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate)); // 同步到 Note 对象
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
mNoteSettingStatusListener.onClockAlertChanged(date, set); // 通知监听器
}
}
/**
* 便
* @param mark
*/
public void markDeleted(boolean mark) {
mIsDeleted = mark;
mIsDeleted = mark; // 设置删除状态
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
mNoteSettingStatusListener.onWidgetChanged(); // 通知监听器
}
}
/**
* 便
* @return
*/
public boolean isDeleted() {
return mIsDeleted;
}
/**
* ID
* @param id ID
*/
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
if (id != mBgColorId) { // 检查颜色是否变更
mBgColorId = id; // 设置背景颜色 ID
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
mNoteSettingStatusListener.onBackgroundColorChanged(); // 通知监听器
}
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id)); // 同步到 Note 对象
}
}
/**
* 便
* @param mode checklist
*/
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mMode != mode) { // 检查模式是否变更
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode); // 通知监听器
}
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
mMode = mode; // 设置模式
mNote.setTextData(TextNote.MODE, String.valueOf(mMode)); // 同步到 Note 对象
}
}
/**
*
* @param type
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
if (type != mWidgetType) { // 检查类型是否变更
mWidgetType = type; // 设置小组件类型
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType)); // 同步到 Note 对象
}
}
/**
* ID
* @param id ID
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
if (id != mWidgetId) { // 检查 ID 是否变更
mWidgetId = id; // 设置小组件 ID
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId)); // 同步到 Note 对象
}
}
/**
* 便
* @param text 便
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
if (!TextUtils.equals(mContent, text)) { // 检查内容是否变更
mContent = text; // 设置便签内容
mNote.setTextData(DataColumns.CONTENT, mContent); // 同步到 Note 对象
mModifiedDate = System.currentTimeMillis(); // 更新修改日期
}
}
/**
* 便
* @param title 便
*/
public void setTitle(String title) {
if (!TextUtils.equals(mTitle, title)) { // 检查标题是否变更
mTitle = title; // 设置便签标题
mNote.setTextData(DataColumns.DATA3, mTitle); // 同步到 Note 对象
mModifiedDate = System.currentTimeMillis(); // 更新修改日期
}
}
/**
* 便
* @return 便
*/
public String getTitle() {
return mTitle;
}
/**
* 便便
* @param phoneNumber
* @param callDate
*/
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate)); // 设置通话日期
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber); // 设置电话号码
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER)); // 设置父文件夹为通话记录文件夹
}
/**
* 便
* @return
*/
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
/**
* 便
* @return 便
*/
public String getContent() {
return mContent;
}
/**
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
* @return ID
*/
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
/**
* ID
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
* @return ID
*/
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
/**
* 便
* @return 便
*/
public int getCheckListMode() {
return mMode;
}
/**
* 便 ID
* @return 便 ID
*/
public long getNoteId() {
return mNoteId;
}
/**
* ID
* @return ID
*/
public long getFolderId() {
return mFolderId;
}
/**
* ID
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* 便
* @return
*/
public boolean isPinned() {
return mPinned;
}
/**
* 便
* @param pinned
*/
public void setPinned(boolean pinned) {
if (mPinned != pinned) { // 检查置顶状态是否变更
mPinned = pinned; // 设置置顶状态
mNote.setNoteValue(NoteColumns.PINNED, String.valueOf(mPinned ? 1 : 0)); // 同步到 Note 对象
}
}
/**
* 便
* @return
*/
public boolean isEncrypted() {
return mEncrypted;
}
/**
* 便
* @param encrypted
*/
public void setEncrypted(boolean encrypted) {
if (mEncrypted != encrypted) { // 检查加密状态是否变更
mEncrypted = encrypted; // 设置加密状态
mNote.setNoteValue(NoteColumns.ENCRYPTED, String.valueOf(mEncrypted ? 1 : 0)); // 同步到 Note 对象
}
}
/**
*
* @param passwordHash
*/
public void setPasswordHash(String passwordHash) {
mPasswordHash = passwordHash; // 设置密码哈希值
mNote.setNoteValue(NoteColumns.PASSWORD_HASH, mPasswordHash); // 同步到 Note 对象
}
/**
*
* @param password
* @return
*/
public boolean verifyPassword(String password) {
return net.micode.notes.tool.EncryptionUtils.generatePasswordHash(password).equals(mPasswordHash);
}
/**
* 便
* 便
*/
public interface NoteSettingChangedListener {
/**
* Called when the background color of current note has just changed
* 便
*/
void onBackgroundColorChanged();
/**
* Called when user set clock
*
* @param date
* @param set
*/
void onClockAlertChanged(long date, boolean set);
/**
* Call when user create note from widget
* 便
*/
void onWidgetChanged();
/**
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
* checklist
* @param oldMode
* @param newMode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}

@ -234,7 +234,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) {

@ -47,29 +47,39 @@ public class DataUtils {
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
Log.d(TAG, "Batch deleting notes, ids: " + ids.toString());
long currentTime = System.currentTimeMillis();
boolean allSuccess = true;
// 为每个ID单独执行删除操作这样即使某个ID删除失败也不会影响其他ID的删除
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue;
}
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
// 创建ContentValues对象设置要更新的值
ContentValues values = new ContentValues();
values.put(NoteColumns.DELETED, 1);
values.put(NoteColumns.DELETED_DATE, currentTime);
// 直接使用ContentResolver的update方法执行更新操作
int rowsUpdated = resolver.update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
values,
null,
null);
// 检查更新是否成功
if (rowsUpdated == 0) {
Log.e(TAG, "Failed to delete note with id: " + id);
allSuccess = false;
} else {
Log.d(TAG, "Successfully deleted note with id: " + id);
}
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;
return allSuccess;
}
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
@ -118,7 +128,7 @@ public class DataUtils {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)},
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLDER)},
null);
int count = 0;
@ -139,7 +149,7 @@ public class DataUtils {
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER,
new String [] {String.valueOf(type)},
null);
@ -184,7 +194,7 @@ public class DataUtils {
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLDER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
@ -292,4 +302,23 @@ public class DataUtils {
}
return snippet;
}
public static String getNoteTitle(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { Notes.DataColumns.DATA3 },
Notes.DataColumns.NOTE_ID + "=? AND " + Notes.DataColumns.MIME_TYPE + "=?",
new String [] { String.valueOf(noteId), Notes.DataConstants.NOTE },
null);
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get note title fails " + e.toString());
} finally {
cursor.close();
}
}
return "";
}
}

@ -0,0 +1,154 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.util.Base64;
import android.util.Log;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
public class EncryptionUtils {
private static final String TAG = "EncryptionUtils";
private static final String ENCRYPTION_ALGORITHM = "AES";
private static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String HASH_ALGORITHM = "SHA-256";
private static final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA256";
private static final int KEY_SIZE = 256;
private static final int IV_SIZE = 16;
private static final int SALT_SIZE = 16;
private static final int ITERATION_COUNT = 10000;
/**
* SHA-256
* @param password
* @return
*/
public static String generatePasswordHash(String password) {
try {
MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
byte[] hash = digest.digest(password.getBytes("UTF-8"));
return Base64.encodeToString(hash, Base64.NO_WRAP);
} catch (Exception e) {
Log.e(TAG, "Error generating password hash", e);
return null;
}
}
/**
*
* @param input
* @param password
* @return salt:iv:encrypted
*/
public static String encrypt(String input, String password) {
try {
// 生成随机盐值
byte[] salt = generateSalt();
// 生成随机IV
byte[] iv = generateIV();
// 从密码生成密钥
SecretKey secretKey = generateKey(password, salt);
// 初始化加密器
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
// 加密数据
byte[] encrypted = cipher.doFinal(input.getBytes("UTF-8"));
// 组合结果salt:iv:encrypted
String saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP);
String ivBase64 = Base64.encodeToString(iv, Base64.NO_WRAP);
String encryptedBase64 = Base64.encodeToString(encrypted, Base64.NO_WRAP);
return saltBase64 + ":" + ivBase64 + ":" + encryptedBase64;
} catch (Exception e) {
Log.e(TAG, "Error encrypting data", e);
return null;
}
}
/**
*
* @param encrypted salt:iv:encrypted
* @param password
* @return
*/
public static String decrypt(String encrypted, String password) {
try {
// 解析加密字符串
String[] parts = encrypted.split(":");
if (parts.length != 3) {
Log.e(TAG, "Invalid encrypted string format");
return null;
}
// 解码各部分
byte[] salt = Base64.decode(parts[0], Base64.NO_WRAP);
byte[] iv = Base64.decode(parts[1], Base64.NO_WRAP);
byte[] encryptedData = Base64.decode(parts[2], Base64.NO_WRAP);
// 从密码生成密钥
SecretKey secretKey = generateKey(password, salt);
// 初始化解密器
Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
// 解密数据
byte[] decrypted = cipher.doFinal(encryptedData);
return new String(decrypted, "UTF-8");
} catch (Exception e) {
Log.e(TAG, "Error decrypting data: " + e.getMessage(), e);
return null;
}
}
/**
*
* @return
*/
private static byte[] generateSalt() {
byte[] salt = new byte[SALT_SIZE];
new SecureRandom().nextBytes(salt);
return salt;
}
/**
* IV
* @return IV
*/
private static byte[] generateIV() {
byte[] iv = new byte[IV_SIZE];
new SecureRandom().nextBytes(iv);
return iv;
}
/**
*
* @param password
* @param salt
* @return
* @throws Exception
*/
private static SecretKey generateKey(String password, byte[] salt) throws Exception {
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATION_COUNT, KEY_SIZE);
SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION);
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, ENCRYPTION_ALGORITHM);
}
}

@ -0,0 +1,359 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class ImageUtils {
private static final String TAG = "ImageUtils";
private static final String IMAGE_DIR = "Notes/Images";
private static final String IMAGE_FORMAT = ".jpg";
private static final int MAX_IMAGE_WIDTH = 1024;
private static final int MAX_IMAGE_HEIGHT = 1024;
private static final int MAX_IMAGE_SIZE = 1024 * 1024; // 1MB
/**
*
* @param context
* @return
*/
public static Bitmap getImageFromClipboard(Context context) {
try {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard == null || !clipboard.hasPrimaryClip()) {
Log.d(TAG, "剪贴板为空");
return null;
}
android.content.ClipData clip = clipboard.getPrimaryClip();
if (clip == null || clip.getItemCount() == 0) {
Log.d(TAG, "剪贴板内容为空");
return null;
}
Log.d(TAG, "剪贴板项目数量: " + clip.getItemCount());
// 检查剪贴板描述
android.content.ClipDescription description = clipboard.getPrimaryClipDescription();
if (description != null) {
Log.d(TAG, "剪贴板描述: " + description.toString());
for (int i = 0; i < description.getMimeTypeCount(); i++) {
Log.d(TAG, "MIME类型 " + i + ": " + description.getMimeType(i));
}
}
for (int i = 0; i < clip.getItemCount(); i++) {
android.content.ClipData.Item item = clip.getItemAt(i);
Log.d(TAG, "处理剪贴板项目 " + i);
// 尝试从URI加载图片标准方式
if (item.getUri() != null) {
Log.d(TAG, "尝试从URI加载图片: " + item.getUri().toString());
try {
Bitmap bitmap = BitmapFactory.decodeStream(
context.getContentResolver().openInputStream(item.getUri()));
if (bitmap != null) {
Log.d(TAG, "成功从URI加载图片: " + bitmap.getWidth() + "x" + bitmap.getHeight());
return bitmap;
} else {
Log.e(TAG, "从URI加载图片失败");
}
} catch (Exception e) {
Log.e(TAG, "从URI加载图片时出错: " + e.getMessage());
e.printStackTrace();
}
} else {
Log.d(TAG, "未找到URI");
}
// 尝试获取Intent可能包含微信截屏的图片信息
if (item.getIntent() != null) {
Log.d(TAG, "找到Intent: " + item.getIntent().toString());
try {
android.content.Intent intent = item.getIntent();
// 检查Intent是否包含图片信息
if (intent.hasExtra(android.content.Intent.EXTRA_STREAM)) {
android.net.Uri uri = (android.net.Uri) intent.getParcelableExtra(android.content.Intent.EXTRA_STREAM);
if (uri != null) {
Log.d(TAG, "从Intent中找到图片URI: " + uri.toString());
Bitmap bitmap = BitmapFactory.decodeStream(
context.getContentResolver().openInputStream(uri));
if (bitmap != null) {
Log.d(TAG, "成功从Intent加载图片: " + bitmap.getWidth() + "x" + bitmap.getHeight());
return bitmap;
}
}
}
} catch (Exception e) {
Log.e(TAG, "从Intent加载图片时出错: " + e.getMessage());
e.printStackTrace();
}
} else {
Log.d(TAG, "未找到Intent");
}
// 尝试从剪贴板直接获取Bitmap针对某些应用的特殊处理
try {
// 注意这是一个尝试因为ClipData.Item没有直接的getBitmap方法
// 但某些应用可能会以特殊方式存储Bitmap
Log.d(TAG, "尝试其他方式从剪贴板获取图片");
// 这里可以添加针对特定应用的特殊处理
} catch (Exception e) {
Log.e(TAG, "尝试其他方式获取图片时出错: " + e.getMessage());
e.printStackTrace();
}
}
Log.d(TAG, "无法从剪贴板获取图片");
return null;
} catch (Exception e) {
Log.e(TAG, "从剪贴板获取图片时出错: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
*
* @param context
* @param bitmap
* @return
*/
public static String saveImage(Context context, Bitmap bitmap) {
if (bitmap == null) {
Log.e(TAG, "Bitmap is null");
return null;
}
Log.d(TAG, "开始保存图片,尺寸: " + bitmap.getWidth() + "x" + bitmap.getHeight());
// 创建图片存储目录
File imageDir;
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
// Android 10+ 使用应用私有存储
imageDir = new File(context.getExternalFilesDir(null), IMAGE_DIR);
Log.d(TAG, "使用应用私有存储: " + imageDir.getAbsolutePath());
} else {
// Android 9 及以下使用外部存储
imageDir = new File(Environment.getExternalStorageDirectory(), IMAGE_DIR);
Log.d(TAG, "使用外部存储: " + imageDir.getAbsolutePath());
}
if (!imageDir.exists()) {
Log.d(TAG, "创建图片存储目录: " + imageDir.getAbsolutePath());
if (!imageDir.mkdirs()) {
Log.e(TAG, "Failed to create image directory");
return null;
}
}
// 生成唯一的文件名
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String fileName = "IMG_" + timeStamp + IMAGE_FORMAT;
File imageFile = new File(imageDir, fileName);
Log.d(TAG, "图片保存路径: " + imageFile.getAbsolutePath());
// 压缩图片
Log.d(TAG, "开始压缩图片");
Bitmap compressedBitmap = compressBitmap(bitmap);
Log.d(TAG, "压缩后图片尺寸: " + compressedBitmap.getWidth() + "x" + compressedBitmap.getHeight());
// 保存图片
Log.d(TAG, "开始写入图片文件");
FileOutputStream fos = new FileOutputStream(imageFile);
boolean compressed = compressedBitmap.compress(Bitmap.CompressFormat.JPEG, 85, fos);
Log.d(TAG, "图片压缩结果: " + compressed);
fos.flush();
fos.close();
Log.d(TAG, "图片保存成功");
// 释放资源
if (compressedBitmap != bitmap) {
compressedBitmap.recycle();
}
Log.d(TAG, "图片保存路径: " + imageFile.getAbsolutePath());
return imageFile.getAbsolutePath();
} catch (Exception e) {
Log.e(TAG, "Error saving image: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
* URI
* @param context
* @param uri URI
* @return
*/
public static String saveImage(Context context, android.net.Uri uri) {
if (uri == null) {
Log.e(TAG, "URI is null");
return null;
}
Log.d(TAG, "开始从URI保存图片: " + uri.toString());
try {
// 从URI加载图片
Bitmap bitmap = BitmapFactory.decodeStream(
context.getContentResolver().openInputStream(uri));
if (bitmap == null) {
Log.e(TAG, "Failed to decode bitmap from URI");
return null;
}
// 使用现有的saveImage方法保存图片
return saveImage(context, bitmap);
} catch (Exception e) {
Log.e(TAG, "Error saving image from URI: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
*
* @param bitmap
* @return
*/
private static Bitmap compressBitmap(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 计算缩放比例
float scaleWidth = ((float) MAX_IMAGE_WIDTH) / width;
float scaleHeight = ((float) MAX_IMAGE_HEIGHT) / height;
float scale = Math.min(scaleWidth, scaleHeight);
// 如果图片不需要压缩,直接返回
if (scale >= 1.0f) {
return bitmap;
}
// 缩放图片
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}
/**
*
* @param path
* @return
*/
public static Bitmap loadImage(String path) {
if (path == null || path.isEmpty()) {
Log.e(TAG, "Image path is null or empty");
return null;
}
Log.d(TAG, "开始加载图片: " + path);
File imageFile = new File(path);
if (!imageFile.exists()) {
Log.e(TAG, "Image file does not exist: " + path);
return null;
}
Log.d(TAG, "图片文件存在,大小: " + imageFile.length() + " bytes");
try {
// 解码图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
Log.d(TAG, "图片原始尺寸: " + options.outWidth + "x" + options.outHeight);
// 计算缩放比例
int scale = 1;
while (options.outWidth / scale > MAX_IMAGE_WIDTH || options.outHeight / scale > MAX_IMAGE_HEIGHT) {
scale *= 2;
}
Log.d(TAG, "图片缩放比例: " + scale);
// 加载缩放后的图片
options.inJustDecodeBounds = false;
options.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
if (bitmap != null) {
Log.d(TAG, "成功加载图片,尺寸: " + bitmap.getWidth() + "x" + bitmap.getHeight());
} else {
Log.e(TAG, "Failed to decode bitmap from file: " + path);
}
return bitmap;
} catch (Exception e) {
Log.e(TAG, "Error loading image: " + e.getMessage());
e.printStackTrace();
return null;
}
}
/**
*
* @param path
* @return
*/
public static boolean deleteImage(String path) {
if (path == null || path.isEmpty()) {
return false;
}
File imageFile = new File(path);
if (!imageFile.exists()) {
return true;
}
return imageFile.delete();
}
/**
*
* @param path
* @return
*/
public static int getImageOrientation(String path) {
try {
ExifInterface exif = new ExifInterface(path);
int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
return orientation;
} catch (Exception e) {
Log.e(TAG, "Error getting image orientation: " + e.getMessage());
return ExifInterface.ORIENTATION_NORMAL;
}
}
}

@ -25,11 +25,13 @@ import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
@ -41,6 +43,7 @@ import java.io.IOException;
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private static final String TAG = "AlarmAlertActivity";
private long mNoteId;
private String mSnippet;
private static final int SNIPPET_PREW_MAX_LEN = 60;
@ -49,38 +52,163 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate called");
// 检查启动来源,避免在不应该显示的时候显示
Intent intent = getIntent();
String action = intent.getAction();
if (action != null) {
Log.d(TAG, "Received action: " + action);
// 跳过所有系统广播触发的提醒
if (action.equals(Intent.ACTION_BOOT_COMPLETED) ||
action.equals(Intent.ACTION_TIME_CHANGED) ||
action.equals(Intent.ACTION_TIMEZONE_CHANGED) ||
action.equals(Intent.ACTION_REBOOT) ||
action.equals(Intent.ACTION_SHUTDOWN) ||
action.equals(Intent.ACTION_USER_PRESENT) ||
action.equals(Intent.ACTION_SCREEN_ON) ||
action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.d(TAG, "Skipping alert for system action: " + action);
finish();
return;
}
}
// 尝试获取noteId但不强制要求
long noteId = 0;
if (intent.getData() != null) {
try {
noteId = Long.valueOf(intent.getData().getPathSegments().get(1));
Log.d(TAG, "Note ID from data: " + noteId);
} catch (Exception e) {
Log.e(TAG, "Error parsing note ID from data", e);
}
}
if (intent.getExtras() != null) {
long extraNoteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
if (extraNoteId > 0) {
noteId = extraNoteId;
Log.d(TAG, "Note ID from extras: " + noteId);
}
}
// 检查是否是由AlarmManager触发的
boolean isFromAlarmManager = false;
if (intent.getExtras() != null) {
isFromAlarmManager = intent.getExtras().getBoolean("from_alarm_manager", false);
}
// 记录启动信息,但不强制要求所有条件都满足
Log.d(TAG, "Alert startup info: noteId=" + noteId + ", isFromAlarmManager=" + isFromAlarmManager);
// 即使笔记不存在,也尝试显示提醒窗口
if (noteId > 0 && !DataUtils.visibleInNoteDatabase(getContentResolver(), noteId, Notes.TYPE_NOTE)) {
Log.d(TAG, "Note not found but will show alert anyway: " + noteId);
}
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Window win = getWindow();
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
// 设置窗口标志,确保在锁屏状态下也能显示,并且能覆盖其他应用
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
// 设置窗口为最高优先级,确保能强制弹出
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// 对于Android O及以上版本使用TYPE_APPLICATION_OVERLAY
win.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
Log.d(TAG, "Using TYPE_APPLICATION_OVERLAY for API 26+");
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
// 对于Android M及以上版本使用TYPE_SYSTEM_ALERT
win.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
Log.d(TAG, "Using TYPE_SYSTEM_ALERT for API 23+");
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
// 对于Android KitKat及以上版本使用TYPE_SYSTEM_ALERT
win.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
Log.d(TAG, "Using TYPE_SYSTEM_ALERT for API 19+");
} else {
// 对于旧版本使用TYPE_PHONE
win.setType(WindowManager.LayoutParams.TYPE_PHONE);
Log.d(TAG, "Using TYPE_PHONE for older APIs");
}
// 设置窗口属性,确保能覆盖其他应用
WindowManager.LayoutParams params = win.getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
params.windowAnimations = android.R.style.Animation_Dialog;
params.alpha = 1.0f;
params.dimAmount = 0.7f;
win.setAttributes(params);
// 确保窗口在最顶层
Log.d(TAG, "Window attributes set");
Intent intent = getIntent();
Log.d(TAG, "Received intent: " + intent);
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
// 确保Intent不为空
if (intent == null) {
Log.e(TAG, "Intent is null");
// 即使Intent为空也显示提醒窗口
mSnippet = getString(R.string.notealert_no_content);
} else {
// 尝试从Intent中获取noteId
if (intent.getData() != null) {
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
Log.d(TAG, "Note ID from data: " + mNoteId);
} catch (Exception e) {
Log.e(TAG, "Error parsing note ID from data", e);
}
}
// 如果从data中获取失败尝试从extras中获取
if (mNoteId == 0) {
mNoteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
Log.d(TAG, "Note ID from extras: " + mNoteId);
}
// 获取笔记内容摘要
if (mNoteId > 0) {
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
if (mSnippet == null) {
mSnippet = getString(R.string.notealert_no_content);
}
} else {
mSnippet = getString(R.string.notealert_no_content);
}
// 限制摘要长度
if (mSnippet.length() > SNIPPET_PREW_MAX_LEN) {
mSnippet = mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info);
}
Log.d(TAG, "Note snippet: " + mSnippet);
}
} catch (Exception e) {
Log.e(TAG, "Error processing intent", e);
// 即使发生异常,也显示提醒窗口
mSnippet = getString(R.string.notealert_no_content);
}
mPlayer = new MediaPlayer();
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
playAlarmSound();
} else {
finish();
}
// 无论笔记是否存在,都显示提醒窗口
Log.d(TAG, "Showing alert dialog regardless of note existence");
showActionDialog();
playAlarmSound();
}
private boolean isScreenOn() {
@ -89,70 +217,130 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
private void playAlarmSound() {
Log.d(TAG, "Playing alarm sound");
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
// 如果没有设置闹钟铃声,使用默认通知铃声
if (url == null) {
Log.d(TAG, "No alarm ringtone, using notification ringtone");
url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_NOTIFICATION);
}
// 如果仍然没有铃声,使用默认铃声
if (url == null) {
Log.d(TAG, "No notification ringtone, using default ringtone");
url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
}
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
if (url != null) {
Log.d(TAG, "Using ringtone: " + url);
try {
// 设置音频流类型
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
mPlayer.setDataSource(this, url);
mPlayer.setLooping(true);
mPlayer.prepare();
mPlayer.start();
Log.d(TAG, "Alarm sound started");
} catch (Exception e) {
Log.e(TAG, "Error playing alarm sound", e);
// 如果播放失败,尝试使用系统默认的闹钟声音
try {
Ringtone ringtone = RingtoneManager.getRingtone(this, url);
if (ringtone != null) {
Log.d(TAG, "Using RingtoneManager to play sound");
ringtone.play();
}
} catch (Exception ex) {
Log.e(TAG, "Error using RingtoneManager", ex);
}
}
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
try {
mPlayer.setDataSource(this, url);
mPlayer.prepare();
mPlayer.setLooping(true);
mPlayer.start();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.w(TAG, "No ringtone available");
}
}
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this);
if (isScreenOn()) {
Log.d(TAG, "Showing action dialog");
try {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this);
// 始终显示"查看笔记"按钮
dialog.setNegativeButton(R.string.notealert_enter, this);
AlertDialog alertDialog = dialog.create();
alertDialog.setOnDismissListener(this);
// 设置对话框属性,确保在锁屏状态下也能显示
alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
alertDialog.show();
Log.d(TAG, "Dialog shown");
} catch (Exception e) {
Log.e(TAG, "Error showing dialog, trying fallback", e);
// 尝试使用简单的对话框作为 fallback
try {
AlertDialog.Builder simpleDialog = new AlertDialog.Builder(this);
simpleDialog.setTitle(R.string.app_name);
simpleDialog.setMessage(getString(R.string.notealert_no_content));
simpleDialog.setPositiveButton(R.string.notealert_ok, this);
AlertDialog alertDialog = simpleDialog.create();
alertDialog.show();
Log.d(TAG, "Fallback dialog shown");
} catch (Exception ex) {
Log.e(TAG, "Error showing fallback dialog", ex);
// 即使对话框显示失败,也不要立即退出,让用户有机会看到通知
Log.d(TAG, "Dialog display failed but activity will continue");
}
}
dialog.show().setOnDismissListener(this);
}
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG, "Dialog button clicked: " + which);
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
Log.d(TAG, "View note button clicked, noteId: " + mNoteId);
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, mNoteId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_SINGLE_TOP
| Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
startActivity(intent);
break;
default:
Log.d(TAG, "OK button clicked");
break;
}
}
public void onDismiss(DialogInterface dialog) {
Log.d(TAG, "Dialog dismissed");
stopAlarmSound();
finish();
}
private void stopAlarmSound() {
Log.d(TAG, "Stopping alarm sound");
if (mPlayer != null) {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
try {
mPlayer.stop();
mPlayer.release();
} catch (Exception e) {
Log.e(TAG, "Error stopping player", e);
} finally {
mPlayer = null;
}
}
}
}

@ -0,0 +1,102 @@
package net.micode.notes.ui;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import net.micode.notes.R;
public class AlarmAlertService extends Service {
private static final String TAG = "AlarmAlertService";
private static final String CHANNEL_ID = "alarm_alert_channel";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Service created");
// 创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Alarm Alert", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Alarm alert notifications");
notificationManager.createNotificationChannel(channel);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "Service started with intent: " + intent);
// 创建前台服务通知
Notification notification = createForegroundNotification();
startForeground(1, notification);
// 启动AlarmAlertActivity
try {
Intent alertIntent = new Intent(this, AlarmAlertActivity.class);
if (intent != null) {
if (intent.getData() != null) {
alertIntent.setData(intent.getData());
}
if (intent.getExtras() != null) {
alertIntent.putExtras(intent.getExtras());
}
}
// 确保添加from_alarm_manager标记
if (!alertIntent.hasExtra("from_alarm_manager")) {
alertIntent.putExtra("from_alarm_manager", true);
Log.d(TAG, "Added from_alarm_manager flag to alert intent");
}
alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(alertIntent);
Log.d(TAG, "AlarmAlertActivity started from service");
} catch (Exception e) {
Log.e(TAG, "Error starting AlarmAlertActivity from service", e);
}
// 停止服务
stopSelf();
return START_NOT_STICKY;
}
private Notification createForegroundNotification() {
Intent notificationIntent = new Intent(this, AlarmAlertActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(this, CHANNEL_ID);
} else {
builder = new Notification.Builder(this);
}
builder.setContentTitle(getString(R.string.app_name))
.setContentText("Alarm alert")
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
return builder.build();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Service destroyed");
}
}

@ -51,12 +51,30 @@ public class AlarmInitReceiver extends BroadcastReceiver {
if (c.moveToFirst()) {
do {
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
long noteId = c.getLong(COLUMN_ID);
Intent sender = new Intent(context, AlarmReceiver.class);
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
AlarmManager alermManager = (AlarmManager) context
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
// 添加noteId作为extra确保AlarmAlertActivity能获取到
sender.putExtra(Intent.EXTRA_UID, noteId);
// 添加标记表明这个Intent是由AlarmManager触发的
sender.putExtra("from_alarm_manager", true);
// 使用笔记的ID作为requestCode确保每个提醒都有唯一的标识
// 创建 PendingIntent适配 Android 12 及以上版本
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, (int) noteId, sender, flags);
AlarmManager alarmManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
// 根据Android版本选择合适的方法
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} else {
alarmManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
}
} while (c.moveToNext());
}
c.close();

@ -16,15 +16,270 @@
package net.micode.notes.ui;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.util.Log;
import net.micode.notes.R;
public class AlarmReceiver extends BroadcastReceiver {
private static final String TAG = "AlarmReceiver";
private static PowerManager.WakeLock sWakeLock;
@Override
public void onReceive(Context context, Intent intent) {
intent.setClass(context, AlarmAlertActivity.class);
Log.d(TAG, "Alarm received: " + intent);
// 获取唤醒锁,确保设备在处理提醒时不会进入睡眠状态
acquireWakeLock(context);
try {
// 确保Intent不为空
if (intent == null) {
Log.e(TAG, "Intent is null");
return;
}
// 检查是否是系统广播,如果是则不启动提醒窗口
String action = intent.getAction();
if (action != null) {
Log.d(TAG, "Received action: " + action);
// 跳过所有系统广播触发的提醒
if (action.equals(Intent.ACTION_BOOT_COMPLETED) ||
action.equals(Intent.ACTION_TIME_CHANGED) ||
action.equals(Intent.ACTION_TIMEZONE_CHANGED) ||
action.equals(Intent.ACTION_REBOOT) ||
action.equals(Intent.ACTION_SHUTDOWN) ||
action.equals(Intent.ACTION_USER_PRESENT) ||
action.equals(Intent.ACTION_SCREEN_ON) ||
action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.d(TAG, "Skipping alert for system action: " + action);
// 对于系统广播,只执行初始化操作,不启动提醒窗口
return;
}
}
// 直接启动提醒窗口,不进行额外检查,确保提醒能够立即触发
Log.d(TAG, "Directly starting alert without extra checks");
startAlarmAlert(context, intent);
} catch (Exception e) {
Log.e(TAG, "Error in onReceive: " + e.getMessage(), e);
// 即使出现异常,也要尝试启动提醒
try {
Log.d(TAG, "Trying to start alert despite exception");
startAlarmAlert(context, intent);
} catch (Exception ex) {
Log.e(TAG, "Error starting alert after exception: " + ex.getMessage(), ex);
}
} finally {
// 释放唤醒锁,避免电池消耗
releaseWakeLock();
}
}
private void acquireWakeLock(Context context) {
try {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (powerManager != null) {
sWakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP,
"AlarmReceiver:WakeLock");
if (sWakeLock != null) {
sWakeLock.acquire(60 * 1000); // 最多持有60秒
Log.d(TAG, "Wake lock acquired");
}
}
} catch (Exception e) {
Log.e(TAG, "Error acquiring wake lock", e);
}
}
private void releaseWakeLock() {
try {
if (sWakeLock != null && sWakeLock.isHeld()) {
sWakeLock.release();
sWakeLock = null;
Log.d(TAG, "Wake lock released");
}
} catch (Exception e) {
Log.e(TAG, "Error releasing wake lock", e);
}
}
/**
* NewAlarmAlertActivity
*/
private void startAlarmAlert(Context context, Intent originalIntent) {
long noteId = 0;
Log.d(TAG, "startAlarmAlert called with context: " + context + ", intent: " + originalIntent);
// 从originalIntent中获取noteId
if (originalIntent != null) {
// 从data中获取
if (originalIntent.getData() != null) {
try {
noteId = Long.valueOf(originalIntent.getData().getPathSegments().get(1));
Log.d(TAG, "Note ID from data: " + noteId);
} catch (Exception e) {
Log.e(TAG, "Error parsing note ID from data", e);
}
}
// 从extras中获取
if (noteId == 0 && originalIntent.getExtras() != null) {
noteId = originalIntent.getLongExtra("note_id", 0);
if (noteId == 0) {
noteId = originalIntent.getLongExtra("noteId", 0);
}
if (noteId == 0) {
noteId = originalIntent.getLongExtra(Intent.EXTRA_UID, 0);
}
Log.d(TAG, "Note ID from extras: " + noteId);
}
}
Log.d(TAG, "Final noteId for alert: " + noteId);
// 立即尝试直接启动NewAlarmAlertActivity
try {
Log.d(TAG, "Starting NewAlarmAlertActivity directly");
Intent alertIntent = new Intent(context, NewAlarmAlertActivity.class);
alertIntent.putExtra("note_id", noteId);
alertIntent.putExtra("from_alarm_manager", true); // 确保添加启动标记
alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
if (context != null) {
context.startActivity(alertIntent);
Log.d(TAG, "NewAlarmAlertActivity started successfully directly");
return;
} else {
Log.e(TAG, "Context is null, cannot start activity");
}
} catch (Exception e) {
Log.e(TAG, "Error starting NewAlarmAlertActivity directly: " + e.getMessage(), e);
}
// 如果直接启动失败,尝试使用前台服务启动
try {
Log.d(TAG, "Trying to start NewAlarmAlertActivity via foreground service");
Intent serviceIntent = new Intent(context, NewAlarmAlertService.class);
serviceIntent.putExtra("note_id", noteId);
serviceIntent.putExtra("from_alarm_manager", true); // 确保添加启动标记
if (context != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
Log.d(TAG, "NewAlarmAlertService started successfully");
return;
} else {
Log.e(TAG, "Context is null, cannot start service");
}
} catch (Exception serviceEx) {
Log.e(TAG, "Service approach failed: " + serviceEx.getMessage(), serviceEx);
}
// 如果服务启动也失败,尝试使用静态方法启动
try {
Log.d(TAG, "Trying to start NewAlarmAlertActivity using static method");
NewAlarmAlertActivity.start(context, noteId);
Log.d(TAG, "NewAlarmAlertActivity started successfully via static method");
return;
} catch (Exception e) {
Log.e(TAG, "Error starting NewAlarmAlertActivity via static method: " + e.getMessage(), e);
}
// 如果所有启动活动的尝试都失败,显示通知式提醒
try {
Log.d(TAG, "All activity start attempts failed, showing notification");
showNotificationAlert(context, noteId);
return;
} catch (Exception notificationEx) {
Log.e(TAG, "Error showing notification: " + notificationEx.getMessage(), notificationEx);
}
// 所有尝试都失败了显示Toast提示
try {
Log.d(TAG, "All attempts failed, showing toast");
if (context != null) {
android.widget.Toast.makeText(context, "提醒时间到", android.widget.Toast.LENGTH_LONG).show();
Log.d(TAG, "Toast shown successfully");
}
} catch (Exception toastEx) {
Log.e(TAG, "Error showing toast: " + toastEx.getMessage(), toastEx);
}
// 所有尝试都失败了
Log.e(TAG, "All attempts to start NewAlarmAlertActivity failed");
}
/**
* 使
*/
private void showNotificationAlert(Context context, long noteId) {
Log.d(TAG, "Showing notification alert for note: " + noteId);
// 创建通知渠道
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(
"alarm_alert_channel",
"Alarm Alert",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Note alarm alerts");
channel.enableVibration(true);
channel.setVibrationPattern(new long[]{0, 1000, 500, 1000});
notificationManager.createNotificationChannel(channel);
}
// 创建通知Intent
Intent intent = new Intent(context, NewAlarmAlertActivity.class);
intent.putExtra("note_id", noteId);
intent.putExtra("from_alarm_manager", true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// 创建 PendingIntent适配 Android 12 及以上版本
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
(int) noteId,
intent,
flags
);
// 创建通知
Notification.Builder builder;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
builder = new Notification.Builder(context, "alarm_alert_channel");
} else {
builder = new Notification.Builder(context);
}
builder.setContentTitle(context.getString(R.string.app_name))
.setContentText("提醒时间到")
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setVibrate(new long[]{0, 1000, 500, 1000})
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_ALL);
// 显示通知
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify((int) noteId, builder.build());
Log.d(TAG, "Notification alert shown");
}
}

@ -29,7 +29,7 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
public class DateTimePickerDialog extends AlertDialog {
private Calendar mDate = Calendar.getInstance();
private boolean mIs24HourView;
@ -58,9 +58,26 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0);
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
setButton(context.getString(R.string.datetime_dialog_ok), this);
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
set24HourView(DateFormat.is24HourFormat(this.getContext()));
setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.datetime_dialog_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mOnDateTimeSetListener != null) {
try {
mOnDateTimeSetListener.OnDateTimeSet((AlertDialog) dialog, mDate.getTimeInMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
dialog.dismiss();
}
});
setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.datetime_dialog_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
set24HourView(DateFormat.is24HourFormat(context));
updateTitle(mDate.getTimeInMillis());
}
@ -77,14 +94,8 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_12HOUR;
setTitle(DateUtils.formatDateTime(getContext(), date, flag));
}
}

@ -0,0 +1,464 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Vibrator;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.tool.DataUtils;
public class NewAlarmAlertActivity extends Activity {
private static final String TAG = "NewAlarmAlertActivity";
private long mNoteId;
private String mSnippet;
private MediaPlayer mPlayer;
private static final int SNIPPET_PREW_MAX_LEN = 60;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate called");
// 设置一个简单的透明布局确保Activity能够正常创建
setContentView(R.layout.alarm_alert);
// 直接初始化并显示对话框
// 检查启动来源,避免在不应该显示的时候显示
if (!isValidLaunch()) {
Log.d(TAG, "Invalid launch, finishing activity");
finish();
return;
}
// 设置窗口属性,确保在任何情况下都能显示
setupWindow();
// 处理Intent数据
processIntent();
// 显示提醒对话框
showAlertDialog();
// 播放提醒声音
playAlarmSound();
// 触发震动提醒
vibrate();
}
/**
*
*/
private boolean isValidLaunch() {
Intent intent = getIntent();
if (intent == null) {
Log.e(TAG, "Intent is null");
return false;
}
// 检查是否是系统广播,如果是则不启动提醒窗口
String action = intent.getAction();
if (action != null) {
Log.d(TAG, "Received action: " + action);
// 跳过所有系统广播触发的提醒
if (action.equals(Intent.ACTION_BOOT_COMPLETED) ||
action.equals(Intent.ACTION_TIME_CHANGED) ||
action.equals(Intent.ACTION_TIMEZONE_CHANGED) ||
action.equals(Intent.ACTION_REBOOT) ||
action.equals(Intent.ACTION_SHUTDOWN) ||
action.equals(Intent.ACTION_USER_PRESENT) ||
action.equals(Intent.ACTION_SCREEN_ON) ||
action.equals(Intent.ACTION_SCREEN_OFF)) {
Log.d(TAG, "Skipping alert for system action: " + action);
return false;
}
}
// 简化检查:只要不是系统广播,就认为是有效的启动
// 这样可以确保任何来自AlarmManager或AlarmReceiver的启动都能通过
Log.d(TAG, "Valid launch: non-system broadcast");
return true;
}
private void setupWindow() {
// 无标题栏
requestWindowFeature(Window.FEATURE_NO_TITLE);
// 获取窗口
final Window win = getWindow();
// 设置窗口标志,确保在锁屏状态下也能显示
win.addFlags(
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
);
// 设置窗口属性
WindowManager.LayoutParams params = win.getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
params.alpha = 1.0f;
params.dimAmount = 0.7f;
win.setAttributes(params);
}
private void processIntent() {
Intent intent = getIntent();
Log.d(TAG, "Processing intent: " + intent);
// 尝试获取noteId
if (intent != null) {
// 从data中获取
if (intent.getData() != null) {
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
Log.d(TAG, "Note ID from data: " + mNoteId);
} catch (Exception e) {
Log.e(TAG, "Error parsing note ID from data", e);
}
}
// 从extras中获取
if (mNoteId == 0 && intent.getExtras() != null) {
mNoteId = intent.getLongExtra("note_id", 0);
if (mNoteId == 0) {
mNoteId = intent.getLongExtra("noteId", 0);
}
if (mNoteId == 0) {
mNoteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
}
Log.d(TAG, "Note ID from extras: " + mNoteId);
}
// 获取笔记内容
if (mNoteId > 0) {
try {
mSnippet = DataUtils.getSnippetById(getContentResolver(), mNoteId);
if (mSnippet == null || mSnippet.isEmpty()) {
mSnippet = "提醒时间到";
} else if (mSnippet.length() > SNIPPET_PREW_MAX_LEN) {
mSnippet = mSnippet.substring(0, SNIPPET_PREW_MAX_LEN) + "...";
}
} catch (Exception e) {
Log.e(TAG, "Error getting note snippet", e);
mSnippet = "提醒时间到";
}
} else {
mSnippet = "提醒时间到";
}
} else {
mSnippet = "提醒时间到";
}
Log.d(TAG, "Final noteId: " + mNoteId + ", snippet: " + mSnippet);
}
private void showAlertDialog() {
Log.d(TAG, "Showing alert dialog");
// 直接在UI线程显示对话框不使用Handler确保立即显示
try {
// 使用现代主题,确保对话框在手机顶部弹出
AlertDialog.Builder builder = new AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert);
builder.setTitle(R.string.app_name);
builder.setMessage(mSnippet);
builder.setCancelable(false);
// 设置按钮
builder.setPositiveButton(R.string.notealert_ok, (dialog, which) -> {
Log.d(TAG, "OK button clicked");
// 停止声音和震动
stopAlarm();
dialog.dismiss();
finish();
});
// 查看笔记按钮
builder.setNegativeButton(R.string.notealert_enter, (dialog, which) -> {
Log.d(TAG, "View note button clicked");
// 停止声音和震动
stopAlarm();
if (mNoteId > 0) {
Intent intent = new Intent(NewAlarmAlertActivity.this, NoteEditActivity.class);
intent.putExtra(Intent.EXTRA_UID, mNoteId);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
dialog.dismiss();
finish();
});
// 添加稍后提醒按钮
builder.setNeutralButton("稍后提醒", (dialog, which) -> {
Log.d(TAG, "Snooze button clicked");
// 停止声音和震动
stopAlarm();
// 设置稍后提醒
scheduleSnooze();
dialog.dismiss();
finish();
});
// 创建并显示对话框
AlertDialog dialog = builder.create();
// 设置对话框属性
dialog.setCanceledOnTouchOutside(false);
// 立即显示对话框,不设置复杂的窗口属性,确保在所有设备上都能显示
dialog.show();
Log.d(TAG, "Alert dialog shown immediately");
} catch (Exception e) {
Log.e(TAG, "Error showing alert dialog", e);
// 如果对话框显示失败,尝试使用系统默认对话框
try {
AlertDialog.Builder simpleBuilder = new AlertDialog.Builder(this);
simpleBuilder.setTitle(R.string.app_name);
simpleBuilder.setMessage("提醒时间到");
simpleBuilder.setCancelable(false);
simpleBuilder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
stopAlarm();
dialog.dismiss();
finish();
});
// 添加稍后提醒按钮
simpleBuilder.setNeutralButton("稍后提醒", (dialog, which) -> {
stopAlarm();
scheduleSnooze();
dialog.dismiss();
finish();
});
AlertDialog simpleDialog = simpleBuilder.create();
simpleDialog.show();
Log.d(TAG, "Simple alert dialog shown");
} catch (Exception ex) {
Log.e(TAG, "Error showing simple dialog", ex);
// 即使对话框完全失败也显示Toast提示
try {
Toast.makeText(this, "提醒时间到", Toast.LENGTH_LONG).show();
Log.d(TAG, "Toast shown as final fallback");
} catch (Exception toastEx) {
Log.e(TAG, "Error showing toast", toastEx);
}
// 停止声音和震动
stopAlarm();
// 3秒后自动退出
new Handler(Looper.getMainLooper()).postDelayed(this::finish, 3000);
}
}
}
private void scheduleSnooze() {
Log.d(TAG, "Scheduling snooze for note: " + mNoteId);
// 设置5分钟后再次提醒
long snoozeTime = System.currentTimeMillis() + 5 * 60 * 1000;
try {
// 创建闹钟Intent
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setAction("net.micode.notes.ALARM_TRIGGERED");
if (mNoteId > 0) {
intent.setData(android.content.ContentUris.withAppendedId(net.micode.notes.data.Notes.CONTENT_NOTE_URI, mNoteId));
intent.putExtra(Intent.EXTRA_UID, mNoteId);
}
intent.putExtra("from_alarm_manager", true);
// 创建PendingIntent
int flags = android.app.PendingIntent.FLAG_UPDATE_CURRENT;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
flags |= android.app.PendingIntent.FLAG_IMMUTABLE;
}
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast(
this,
(int) mNoteId,
intent,
flags
);
// 设置闹钟
android.app.AlarmManager alarmManager = (android.app.AlarmManager) getSystemService(ALARM_SERVICE);
if (alarmManager != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent);
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
alarmManager.setExact(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent);
} else {
alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, snoozeTime, pendingIntent);
}
Log.d(TAG, "Snooze alarm scheduled for: " + snoozeTime);
}
} catch (Exception e) {
Log.e(TAG, "Error scheduling snooze", e);
}
}
private void playAlarmSound() {
Log.d(TAG, "Playing alarm sound");
try {
// 获取默认闹钟铃声
Uri alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
// 如果没有设置闹钟铃声,使用通知铃声
if (alarmUri == null) {
alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
}
// 如果仍然没有,使用默认铃声
if (alarmUri == null) {
alarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
}
if (alarmUri != null) {
Log.d(TAG, "Using ringtone: " + alarmUri);
mPlayer = new MediaPlayer();
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
mPlayer.setDataSource(this, alarmUri);
mPlayer.setLooping(true);
mPlayer.prepare();
mPlayer.start();
Log.d(TAG, "Alarm sound started");
} else {
Log.w(TAG, "No ringtone available");
}
} catch (Exception e) {
Log.e(TAG, "Error playing alarm sound", e);
}
}
private void vibrate() {
Log.d(TAG, "Starting vibration");
try {
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null) {
// 检查设备是否支持震动
if (vibrator.hasVibrator()) {
// 定义震动模式:[延迟, 震动时长, 间隔, 震动时长, ...]
long[] pattern = {0, 1000, 500, 1000, 500, 1000};
// 重复模式:-1表示不重复0表示从索引0开始重复
vibrator.vibrate(pattern, -1);
Log.d(TAG, "Vibration started");
} else {
Log.w(TAG, "Device does not support vibration");
}
} else {
Log.e(TAG, "Vibrator service not available");
}
} catch (Exception e) {
Log.e(TAG, "Error starting vibration", e);
}
}
private void stopAlarm() {
Log.d(TAG, "Stopping alarm");
// 停止播放声音
if (mPlayer != null) {
try {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
Log.d(TAG, "Alarm sound stopped");
} catch (Exception e) {
Log.e(TAG, "Error stopping alarm sound", e);
}
}
// 停止震动
try {
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null) {
vibrator.cancel();
Log.d(TAG, "Vibration stopped");
}
} catch (Exception e) {
Log.e(TAG, "Error stopping vibration", e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy called");
// 停止所有提醒
stopAlarm();
}
// 静态方法,用于启动提醒
public static void start(Context context, long noteId) {
Log.d(TAG, "Starting NewAlarmAlertActivity for note: " + noteId);
Intent intent = new Intent(context, NewAlarmAlertActivity.class);
intent.putExtra("note_id", noteId);
intent.putExtra("from_alarm_manager", true); // 确保添加启动标记
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(intent);
Log.d(TAG, "NewAlarmAlertActivity started successfully");
} catch (Exception e) {
Log.e(TAG, "Error starting NewAlarmAlertActivity", e);
// 如果直接启动失败,尝试使用服务
try {
Intent serviceIntent = new Intent(context, NewAlarmAlertService.class);
serviceIntent.putExtra("note_id", noteId);
serviceIntent.putExtra("from_alarm_manager", true); // 确保添加启动标记
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
Log.d(TAG, "NewAlarmAlertService started as fallback");
} catch (Exception ex) {
Log.e(TAG, "Error starting NewAlarmAlertService", ex);
// 如果服务也启动失败尝试显示一个简单的Toast提示
try {
android.widget.Toast.makeText(context, "提醒时间到", android.widget.Toast.LENGTH_LONG).show();
Log.d(TAG, "Toast shown as final fallback");
} catch (Exception toastEx) {
Log.e(TAG, "Error showing toast", toastEx);
}
}
}
}
}

@ -0,0 +1,123 @@
package net.micode.notes.ui;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import net.micode.notes.R;
public class NewAlarmAlertService extends Service {
private static final String TAG = "NewAlarmAlertService";
private static final String CHANNEL_ID = "new_alarm_alert_channel";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Service created");
// 创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "New Alarm Alert", NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("New alarm alert notifications");
notificationManager.createNotificationChannel(channel);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "Service started with intent: " + intent);
// 创建前台服务通知
Notification notification = createForegroundNotification();
startForeground(1, notification);
// 启动NewAlarmAlertActivity
try {
Intent alertIntent = new Intent(this, NewAlarmAlertActivity.class);
if (intent != null && intent.getExtras() != null) {
alertIntent.putExtras(intent.getExtras());
}
// 确保添加from_alarm_manager标记
if (!alertIntent.hasExtra("from_alarm_manager")) {
alertIntent.putExtra("from_alarm_manager", true);
Log.d(TAG, "Added from_alarm_manager flag to alert intent");
}
// 添加必要的标志确保在Android 13+中能够启动
alertIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
Intent.FLAG_ACTIVITY_NO_HISTORY
);
// 对于Android 13+使用ContextCompat.startActivity
try {
startActivity(alertIntent);
Log.d(TAG, "NewAlarmAlertActivity started from service");
} catch (Exception e) {
Log.e(TAG, "Error starting NewAlarmAlertActivity directly, trying with ContextCompat", e);
// 尝试使用ContextCompat启动
androidx.core.content.ContextCompat.startActivity(this, alertIntent, null);
Log.d(TAG, "NewAlarmAlertActivity started from service using ContextCompat");
}
} catch (Exception e) {
Log.e(TAG, "Error starting NewAlarmAlertActivity from service", e);
}
// 延迟停止服务确保Activity有足够时间启动
new android.os.Handler().postDelayed(new Runnable() {
@Override
public void run() {
stopSelf();
}
}, 2000);
return START_NOT_STICKY;
}
private Notification createForegroundNotification() {
Intent notificationIntent = new Intent(this, NewAlarmAlertActivity.class);
// 创建 PendingIntent适配 Android 12 及以上版本
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags);
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(this, CHANNEL_ID);
} else {
builder = new Notification.Builder(this);
}
builder.setContentTitle(getString(R.string.app_name))
.setContentText("Alarm alert")
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
return builder.build();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Service destroyed");
}
}

File diff suppressed because it is too large Load Diff

@ -16,7 +16,12 @@
package net.micode.notes.ui;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
@ -31,11 +36,16 @@ import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.widget.EditText;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.tool.ImageUtils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NoteEditText extends EditText {
private static final String TAG = "NoteEditText";
@ -52,6 +62,37 @@ public class NoteEditText extends EditText {
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
*
* @param text
* @return
*/
private boolean isImagePath(String text) {
if (text == null || text.isEmpty()) {
return false;
}
// 检查文本是否是有效的文件路径
File file = new File(text);
if (!file.exists()) {
return false;
}
// 检查文件是否是图片类型
String fileName = file.getName();
String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
// 常见的图片文件扩展名
String[] imageExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
for (String ext : imageExtensions) {
if (extension.equals(ext)) {
return true;
}
}
return false;
}
/**
* Call by the {@link NoteEditActivity} to delete or add edit text
@ -75,7 +116,18 @@ public class NoteEditText extends EditText {
void onTextChange(int index, boolean hasText);
}
/**
* Call by the {@link NoteEditActivity} to update format buttons state
*/
public interface OnSelectionChangedListener {
/**
* Called when the selection in the edit text changes
*/
void onSelectionChanged(int start, int end);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
private OnSelectionChangedListener mOnSelectionChangedListener;
public NoteEditText(Context context) {
super(context, null);
@ -90,6 +142,18 @@ public class NoteEditText extends EditText {
mOnTextViewChangeListener = listener;
}
public void setOnSelectionChangedListener(OnSelectionChangedListener listener) {
mOnSelectionChangedListener = listener;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mOnSelectionChangedListener != null) {
mOnSelectionChangedListener.onSelectionChanged(selStart, selEnd);
}
}
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
@ -214,4 +278,529 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
// 处理粘贴操作,检查剪贴板是否有图片
if (handlePasteImage()) {
return true;
}
}
return super.onTextContextMenuItem(id);
}
/**
*
* @param imagePath
*/
public void insertImage(final String imagePath) {
Log.d(TAG, "开始插入图片,路径: " + imagePath);
// 确保在主线程中操作
post(new Runnable() {
@Override
public void run() {
doInsertImage(imagePath);
}
});
}
/**
*
* @param imagePath
*/
private void doInsertImage(String imagePath) {
if (TextUtils.isEmpty(imagePath)) {
Log.e(TAG, "图片路径为空");
Toast.makeText(getContext(), "图片路径为空", Toast.LENGTH_SHORT).show();
return;
}
// 检查文件是否存在
java.io.File imageFile = new java.io.File(imagePath);
if (!imageFile.exists()) {
Log.e(TAG, "图片文件不存在: " + imagePath);
Toast.makeText(getContext(), "图片文件不存在: " + imagePath, Toast.LENGTH_SHORT).show();
return;
}
Log.d(TAG, "图片文件存在,大小: " + imageFile.length() + " bytes");
// 获取当前文本
android.text.Editable editable = getText();
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
// 添加图片标记,以便保存时能够正确保存图片路径
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
editable.insert(start, imageTag);
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
// 处理图片标记,将其转换为 ImageSpan
processImageTags();
// 强制刷新界面
invalidate();
requestLayout();
Log.d(TAG, "强制刷新界面和布局");
// 显示成功提示
Toast.makeText(getContext(), "图片插入成功", Toast.LENGTH_SHORT).show();
Log.d(TAG, "图片插入成功,路径: " + imagePath);
Log.d(TAG, "插入后文本长度: " + getText().length());
}
/**
* ImageSpan
*/
public void processImageTags() {
android.text.Editable editable = getText();
String text = editable.toString();
Pattern pattern = Pattern.compile("\\[IMAGE\\](.*?)\\[/IMAGE\\]");
Matcher matcher = pattern.matcher(text);
// 从后往前处理,避免索引变化
while (matcher.find()) {
String imagePath = matcher.group(1);
int start = matcher.start();
int end = matcher.end();
// 加载图片
try {
android.graphics.BitmapFactory.Options options = new android.graphics.BitmapFactory.Options();
options.inSampleSize = 4;
options.inPreferredConfig = android.graphics.Bitmap.Config.RGB_565;
android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeFile(imagePath, options);
if (bitmap != null) {
// 保留原始标记,直接在标记上设置 ImageSpan
// 创建 ImageSpan
android.text.style.ImageSpan imageSpan = new android.text.style.ImageSpan(getContext(), bitmap);
// 设置 ImageSpan
editable.setSpan(imageSpan, start, end, android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Log.d(TAG, "成功处理图片标记: " + imagePath);
}
} catch (Exception e) {
Log.e(TAG, "处理图片标记时出错: " + e.getMessage(), e);
}
}
// 强制刷新界面
invalidate();
requestLayout();
}
/**
*
* @return
*/
private boolean handlePasteImage() {
Log.d(TAG, "开始处理粘贴图片");
// 尝试使用专门的方法从剪贴板获取图片(针对虚拟机环境)
Log.d(TAG, "尝试使用专门的方法从剪贴板获取图片");
Bitmap bitmap = ImageUtils.getImageFromClipboard(getContext());
if (bitmap != null) {
Log.d(TAG, "成功从剪贴板获取图片: " + bitmap.getWidth() + "x" + bitmap.getHeight());
// 保存图片到本地存储
Log.d(TAG, "尝试保存图片");
String imagePath = ImageUtils.saveImage(getContext(), bitmap);
if (imagePath != null) {
Log.d(TAG, "成功保存图片到: " + imagePath);
// 生成图片标记
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
Log.d(TAG, "生成图片标记: " + imageTag);
// 将图片标记插入到文本中
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
getText().insert(start, imageTag);
Log.d(TAG, "成功插入图片标记");
Log.d(TAG, "插入后文本长度: " + getText().length());
Log.d(TAG, "插入后文本内容: " + getText().toString());
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length()));
// 强制刷新界面
invalidate();
Log.d(TAG, "强制刷新界面");
Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show();
Log.d(TAG, "图片粘贴成功,路径: " + imagePath);
return true;
} else {
Log.e(TAG, "保存图片失败");
Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show();
return true;
}
} else {
Log.d(TAG, "使用专门方法从剪贴板获取图片失败,尝试使用传统方法");
}
// 获取剪贴板管理器
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
// 检查剪贴板是否有内容
if (clipboard == null) {
Log.e(TAG, "剪贴板管理器为空");
Toast.makeText(getContext(), "剪贴板管理器不可用", Toast.LENGTH_SHORT).show();
return false;
}
if (!clipboard.hasPrimaryClip()) {
Log.e(TAG, "剪贴板为空");
Toast.makeText(getContext(), "剪贴板为空", Toast.LENGTH_SHORT).show();
return false;
}
// 获取剪贴板内容
ClipData clip = clipboard.getPrimaryClip();
if (clip == null) {
Log.e(TAG, "剪贴板内容为空");
Toast.makeText(getContext(), "剪贴板内容为空", Toast.LENGTH_SHORT).show();
return false;
}
Log.d(TAG, "剪贴板项目数量: " + clip.getItemCount());
// 检查剪贴板描述
ClipDescription description = clipboard.getPrimaryClipDescription();
if (description != null) {
Log.d(TAG, "剪贴板描述: " + description.toString());
for (int i = 0; i < description.getMimeTypeCount(); i++) {
Log.d(TAG, "MIME类型 " + i + ": " + description.getMimeType(i));
}
} else {
Log.e(TAG, "剪贴板描述为空");
}
// 检查是否有图片
boolean foundContent = false;
for (int i = 0; i < clip.getItemCount(); i++) {
Log.d(TAG, "处理剪贴板项目 " + i);
ClipData.Item item = clip.getItemAt(i);
// 检查是否有URI可能是图片文件
if (item.getUri() != null) {
Log.d(TAG, "找到URI: " + item.getUri().toString());
foundContent = true;
try {
// 尝试从URI加载图片
Log.d(TAG, "尝试从URI加载图片: " + item.getUri().toString());
Bitmap bitmapFromUri = BitmapFactory.decodeStream(
getContext().getContentResolver().openInputStream(item.getUri()));
if (bitmapFromUri != null) {
Log.d(TAG, "成功加载图片: " + bitmapFromUri.getWidth() + "x" + bitmapFromUri.getHeight());
// 保存图片到本地存储
Log.d(TAG, "尝试保存图片");
String imagePath = ImageUtils.saveImage(getContext(), bitmapFromUri);
if (imagePath != null) {
Log.d(TAG, "成功保存图片到: " + imagePath);
// 生成图片标记
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
Log.d(TAG, "生成图片标记: " + imageTag);
// 将图片标记插入到文本中
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
getText().insert(start, imageTag);
Log.d(TAG, "成功插入图片标记");
Log.d(TAG, "插入后文本长度: " + getText().length());
Log.d(TAG, "插入后文本内容: " + getText().toString());
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length()));
// 强制刷新界面
invalidate();
Log.d(TAG, "强制刷新界面");
Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show();
Log.d(TAG, "图片粘贴成功,路径: " + imagePath);
return true;
} else {
Log.e(TAG, "保存图片失败");
Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show();
return true;
}
} else {
Log.e(TAG, "从URI加载图片失败");
Toast.makeText(getContext(), "无法加载图片", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "加载图片时出错: " + e.getMessage());
e.printStackTrace();
Toast.makeText(getContext(), "加载图片时出错: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
} else {
Log.d(TAG, "未找到URI");
}
// 尝试获取文本(可能包含图片路径或其他信息)
if (item.getText() != null) {
String text = item.getText().toString();
Log.d(TAG, "找到文本: " + text);
foundContent = true;
// 检查文本是否是图片路径
if (isImagePath(text)) {
Log.d(TAG, "文本是图片路径: " + text);
// 尝试从路径加载图片
try {
Bitmap bitmapFromPath = BitmapFactory.decodeFile(text);
if (bitmapFromPath != null) {
Log.d(TAG, "成功从路径加载图片: " + bitmapFromPath.getWidth() + "x" + bitmapFromPath.getHeight());
// 保存图片到本地存储
String imagePath = ImageUtils.saveImage(getContext(), bitmapFromPath);
if (imagePath != null) {
Log.d(TAG, "成功保存图片到: " + imagePath);
// 生成图片标记
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
Log.d(TAG, "生成图片标记: " + imageTag);
// 将图片标记插入到文本中
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
getText().insert(start, imageTag);
Log.d(TAG, "成功插入图片标记");
Log.d(TAG, "插入后文本长度: " + getText().length());
Log.d(TAG, "插入后文本内容: " + getText().toString());
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length()));
// 强制刷新界面
invalidate();
Log.d(TAG, "强制刷新界面");
Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show();
Log.d(TAG, "图片粘贴成功,路径: " + imagePath);
return true;
} else {
Log.e(TAG, "保存图片失败");
Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show();
return true;
}
} else {
Log.e(TAG, "从路径加载图片失败: " + text);
// 图片加载失败,直接粘贴文本内容
int start = getSelectionStart();
getText().insert(start, text);
setSelection(start + text.length());
Toast.makeText(getContext(), "图片加载失败,已粘贴路径", Toast.LENGTH_SHORT).show();
Log.d(TAG, "图片加载失败,已粘贴路径");
return true;
}
} catch (Exception e) {
Log.e(TAG, "从路径加载图片时出错: " + e.getMessage());
e.printStackTrace();
// 加载失败,直接粘贴文本内容
int start = getSelectionStart();
getText().insert(start, text);
setSelection(start + text.length());
Toast.makeText(getContext(), "图片加载失败,已粘贴路径", Toast.LENGTH_SHORT).show();
Log.d(TAG, "图片加载失败,已粘贴路径");
return true;
}
} else {
// 不是图片路径,直接粘贴文本内容
int start = getSelectionStart();
getText().insert(start, text);
setSelection(start + text.length());
Toast.makeText(getContext(), "文本粘贴成功", Toast.LENGTH_SHORT).show();
Log.d(TAG, "文本粘贴成功");
return true;
}
} else {
Log.d(TAG, "未找到文本");
}
// 尝试获取Intent可能包含图片信息
if (item.getIntent() != null) {
Log.d(TAG, "找到Intent: " + item.getIntent().toString());
foundContent = true;
// 尝试从Intent中提取图片
try {
android.content.Intent intent = item.getIntent();
if (intent.hasExtra(android.content.Intent.EXTRA_STREAM)) {
android.net.Uri uri = (android.net.Uri) intent.getParcelableExtra(android.content.Intent.EXTRA_STREAM);
if (uri != null) {
Log.d(TAG, "从Intent中找到图片URI: " + uri.toString());
// 尝试从URI加载图片
Bitmap bitmapFromIntent = BitmapFactory.decodeStream(
getContext().getContentResolver().openInputStream(uri));
if (bitmapFromIntent != null) {
Log.d(TAG, "成功从Intent加载图片: " + bitmapFromIntent.getWidth() + "x" + bitmapFromIntent.getHeight());
// 保存图片到本地存储
String imagePath = ImageUtils.saveImage(getContext(), bitmapFromIntent);
if (imagePath != null) {
Log.d(TAG, "成功保存图片到: " + imagePath);
// 生成图片标记
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
Log.d(TAG, "生成图片标记: " + imageTag);
// 将图片标记插入到文本中
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
getText().insert(start, imageTag);
Log.d(TAG, "成功插入图片标记");
Log.d(TAG, "插入后文本长度: " + getText().length());
Log.d(TAG, "插入后文本内容: " + getText().toString());
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length()));
// 强制刷新界面
invalidate();
Log.d(TAG, "强制刷新界面");
Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show();
Log.d(TAG, "图片粘贴成功,路径: " + imagePath);
return true;
} else {
Log.e(TAG, "保存图片失败");
Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show();
return true;
}
} else {
Log.e(TAG, "从Intent URI加载图片失败");
}
}
}
} catch (Exception e) {
Log.e(TAG, "从Intent加载图片时出错: " + e.getMessage());
e.printStackTrace();
}
} else {
Log.d(TAG, "未找到Intent");
}
// 尝试获取HTML文本可能包含图片信息
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
if (item.getHtmlText() != null) {
String htmlText = item.getHtmlText().toString();
Log.d(TAG, "找到HTML文本: " + htmlText);
foundContent = true;
// 尝试从HTML中提取图片信息
if (htmlText.contains("<img")) {
Log.d(TAG, "HTML文本中包含图片标签");
Toast.makeText(getContext(), "HTML文本中包含图片标签但暂不支持", Toast.LENGTH_SHORT).show();
}
} else {
Log.d(TAG, "未找到HTML文本");
}
}
// 尝试使用ContentResolver获取剪贴板中的图片
try {
Log.d(TAG, "尝试使用ContentResolver获取剪贴板中的图片");
android.content.ContentResolver resolver = getContext().getContentResolver();
// 尝试获取剪贴板中的图片
android.net.Uri clipboardUri = null;
if (clip.getDescription().hasMimeType("image/*")) {
clipboardUri = item.getUri();
} else if (clip.getDescription().hasMimeType("text/html")) {
// 尝试从HTML中提取图片URI
Log.d(TAG, "尝试从HTML中提取图片URI");
}
if (clipboardUri != null) {
Log.d(TAG, "使用ContentResolver找到图片URI: " + clipboardUri.toString());
Bitmap bitmapFromContentResolver = BitmapFactory.decodeStream(
resolver.openInputStream(clipboardUri));
if (bitmapFromContentResolver != null) {
Log.d(TAG, "成功从ContentResolver加载图片: " + bitmapFromContentResolver.getWidth() + "x" + bitmapFromContentResolver.getHeight());
// 保存图片到本地存储
String imagePath = ImageUtils.saveImage(getContext(), bitmapFromContentResolver);
if (imagePath != null) {
Log.d(TAG, "成功保存图片到: " + imagePath);
// 生成图片标记
String imageTag = "[IMAGE]" + imagePath + "[/IMAGE]";
Log.d(TAG, "生成图片标记: " + imageTag);
// 将图片标记插入到文本中
int start = getSelectionStart();
Log.d(TAG, "插入位置: " + start);
getText().insert(start, imageTag);
Log.d(TAG, "成功插入图片标记");
Log.d(TAG, "插入后文本长度: " + getText().length());
Log.d(TAG, "插入后文本内容: " + getText().toString());
// 移动光标到图片标记后面
setSelection(start + imageTag.length());
Log.d(TAG, "成功移动光标到位置: " + (start + imageTag.length()));
// 强制刷新界面
invalidate();
Log.d(TAG, "强制刷新界面");
Toast.makeText(getContext(), "图片粘贴成功,路径: " + imagePath, Toast.LENGTH_LONG).show();
Log.d(TAG, "图片粘贴成功,路径: " + imagePath);
return true;
} else {
Log.e(TAG, "保存图片失败");
Toast.makeText(getContext(), "图片保存失败", Toast.LENGTH_SHORT).show();
return true;
}
} else {
Log.e(TAG, "从ContentResolver加载图片失败");
}
} else {
Log.d(TAG, "未找到ContentResolver图片URI");
}
} catch (Exception e) {
Log.e(TAG, "使用ContentResolver加载图片时出错: " + e.getMessage());
e.printStackTrace();
}
}
// 如果没有找到图片,显示提示
if (foundContent) {
Log.e(TAG, "找到剪贴板内容,但无法识别为图片或文本");
Toast.makeText(getContext(), "无法识别剪贴板内容", Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "剪贴板中没有可识别的内容");
Toast.makeText(getContext(), "剪贴板中没有可识别的内容", Toast.LENGTH_SHORT).show();
}
return false;
}
}

@ -27,7 +27,7 @@ import net.micode.notes.tool.DataUtils;
public class NoteItemData {
static final String [] PROJECTION = new String [] {
public static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
@ -40,6 +40,8 @@ public class NoteItemData {
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.PINNED,
NoteColumns.ENCRYPTED,
};
private static final int ID_COLUMN = 0;
@ -54,6 +56,8 @@ 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 PINNED_COLUMN = 12;
private static final int ENCRYPTED_COLUMN = 13;
private long mId;
private long mAlertDate;
@ -64,9 +68,12 @@ public class NoteItemData {
private int mNotesCount;
private long mParentId;
private String mSnippet;
private String mTitle;
private int mType;
private int mWidgetId;
private int mWidgetType;
private boolean mPinned;
private boolean mEncrypted;
private String mName;
private String mPhoneNumber;
@ -91,6 +98,11 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false;
mEncrypted = (cursor.getInt(ENCRYPTED_COLUMN) > 0) ? true : false;
// 获取标题信息
mTitle = DataUtils.getNoteTitle(context.getContentResolver(), mId);
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
@ -217,6 +229,18 @@ public class NoteItemData {
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
public boolean isPinned() {
return mPinned;
}
public boolean isEncrypted() {
return mEncrypted;
}
public String getTitle() {
return mTitle;
}
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);

File diff suppressed because it is too large Load Diff

@ -37,6 +37,7 @@ public class NotesListAdapter extends CursorAdapter {
private HashMap<Integer, Boolean> mSelectedIndex;
private int mNotesCount;
private boolean mChoiceMode;
private String mSearchQuery;
public static class AppWidgetAttribute {
public int widgetId;
@ -48,6 +49,12 @@ public class NotesListAdapter extends CursorAdapter {
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
mSearchQuery = "";
}
public void setSearchQuery(String searchQuery) {
mSearchQuery = searchQuery;
notifyDataSetChanged();
}
@Override
@ -68,13 +75,23 @@ public class NotesListAdapter extends CursorAdapter {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
public void toggleItemChecked(final int position) {
Boolean currentState = mSelectedIndex.get(position);
boolean newState = (currentState == null) ? true : !currentState;
mSelectedIndex.put(position, newState);
notifyDataSetChanged();
}
public boolean isInChoiceMode() {
return mChoiceMode;
}
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
// 只有在从选择模式切换到非选择模式时才清除mSelectedIndex集合
if (!mode) {
mSelectedIndex.clear();
}
mChoiceMode = mode;
}
@ -91,17 +108,32 @@ public class NotesListAdapter extends CursorAdapter {
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
Log.d(TAG, "getSelectedItemIds called, mSelectedIndex size: " + mSelectedIndex.size());
// 获取整个Cursor对象
Cursor cursor = getCursor();
Log.d(TAG, "Cursor obtained: " + (cursor != null));
if (cursor != null) {
for (Integer position : mSelectedIndex.keySet()) {
Log.d(TAG, "Checking position: " + position + ", selected: " + mSelectedIndex.get(position));
if (mSelectedIndex.get(position) == true) {
// 将Cursor移动到指定位置
if (cursor.moveToPosition(position)) {
// 从Cursor中获取笔记的实际ID使用ID_COLUMN索引0
long id = cursor.getLong(0);
Log.d(TAG, "Found note with id: " + id);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
Log.d(TAG, "Added note id to set: " + id);
}
} else {
Log.e(TAG, "Failed to move cursor to position: " + position);
}
}
}
}
Log.d(TAG, "Returning itemSet with size: " + itemSet.size() + ", ids: " + itemSet);
return itemSet;
}

@ -17,7 +17,7 @@
package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.content.Intent;
import android.view.View;
import android.widget.CheckBox;
import android.widget.ImageView;
@ -27,7 +27,6 @@ import android.widget.TextView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
public class NotesListItem extends LinearLayout {
@ -47,7 +46,10 @@ public class NotesListItem extends LinearLayout {
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
/**
*
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
@ -57,6 +59,120 @@ public class NotesListItem extends LinearLayout {
}
mItemData = data;
// 移除所有监听器确保不会与Activity的监听器冲突
setOnTouchListener(null);
setOnLongClickListener(null);
// 添加点击监听器
setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
// 在选择模式下,切换复选框状态
boolean newCheckedState = !mCheckBox.isChecked();
mCheckBox.setChecked(newCheckedState);
// 通知适配器更新选择状态
if (context instanceof NotesListActivity) {
NotesListActivity activity = (NotesListActivity) context;
// 通过反射获取适配器并更新选择状态
try {
// 获取GridView
android.widget.GridView gridView = (android.widget.GridView) activity.findViewById(net.micode.notes.R.id.notes_grid);
if (gridView != null) {
// 获取适配器
android.widget.ListAdapter adapter = gridView.getAdapter();
if (adapter instanceof net.micode.notes.ui.NotesListAdapter) {
net.micode.notes.ui.NotesListAdapter notesAdapter = (net.micode.notes.ui.NotesListAdapter) adapter;
// 查找当前项在适配器中的位置
android.database.Cursor cursor = (android.database.Cursor) notesAdapter.getCursor();
if (cursor != null) {
int position = -1;
for (int i = 0; i < cursor.getCount(); i++) {
if (cursor.moveToPosition(i)) {
long noteId = cursor.getLong(0); // 假设ID是第一列
if (noteId == data.getId()) {
position = i;
break;
}
}
}
if (position != -1) {
// 切换选择状态
notesAdapter.toggleItemChecked(position);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} else if (context instanceof NotesListActivity) {
// 非选择模式下,正常打开便签或文件夹
NotesListActivity activity = (NotesListActivity) context;
if (data.getType() == Notes.TYPE_FOLDER) {
activity.openFolder(data);
} else if (data.getType() == Notes.TYPE_NOTE) {
// 检查便签是否加密
boolean isEncrypted = data.isEncrypted();
// 加载便签并再次检查加密状态,确保准确性
net.micode.notes.model.WorkingNote note = net.micode.notes.model.WorkingNote.load(activity, data.getId());
if (note != null) {
isEncrypted = note.isEncrypted();
}
if (isEncrypted) {
// 弹出密码输入对话框
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(activity);
builder.setTitle("输入密码");
builder.setIcon(android.R.drawable.ic_dialog_alert);
final android.widget.EditText input = new android.widget.EditText(activity);
input.setHint("请输入密码");
input.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
builder.setView(input);
builder.setPositiveButton("确定",
new android.content.DialogInterface.OnClickListener() {
public void onClick(android.content.DialogInterface dialog, int which) {
String password = input.getText().toString();
if (!android.text.TextUtils.isEmpty(password)) {
// 验证密码
net.micode.notes.model.WorkingNote verifyNote = net.micode.notes.model.WorkingNote.load(activity, data.getId());
if (verifyNote != null && verifyNote.verifyPassword(password)) {
// 密码正确,启动 NoteEditActivity 并传递密码
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, data.getId());
intent.putExtra("password", password);
activity.startActivityForResult(intent, 102);
} else {
// 密码错误,显示错误提示
android.widget.Toast.makeText(activity, "密码错误", android.widget.Toast.LENGTH_SHORT).show();
}
} else {
android.widget.Toast.makeText(activity, "密码不能为空", android.widget.Toast.LENGTH_SHORT).show();
}
}
});
builder.setNegativeButton("取消", null);
// 设置对话框不可取消,确保用户必须输入密码或点击取消
builder.setCancelable(false);
builder.show();
} else {
// 非加密便签,直接启动 NoteEditActivity
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, data.getId());
activity.startActivityForResult(intent, 102);
}
}
}
}
});
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
@ -68,13 +184,19 @@ public class NotesListItem extends LinearLayout {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
String title = DataUtils.getFormattedSnippet(data.getSnippet());
// 如果有提醒,添加剩余提醒时间
if (data.hasAlert()) {
String reminderTime = getRemainingReminderTime(context, data.getAlertDate());
if (!reminderTime.isEmpty()) {
title += " " + reminderTime;
}
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
mTitle.setText(title);
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
@ -85,38 +207,118 @@ public class NotesListItem extends LinearLayout {
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
// 优先显示标题,如果没有标题则显示内容摘要
String title = data.getTitle();
if (title == null || title.isEmpty()) {
if (data.isEncrypted()) {
title = "[已加密]";
} else {
title = DataUtils.getFormattedSnippet(data.getSnippet());
}
}
// 如果有提醒,添加剩余提醒时间
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
String reminderTime = getRemainingReminderTime(context, data.getAlertDate());
if (!reminderTime.isEmpty()) {
title += " " + reminderTime;
}
}
mTitle.setText(title);
if (data.isEncrypted()) {
// 加密状态显示锁图标
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
mAlert.setVisibility(View.VISIBLE);
} else {
// 未加密的笔记右下角不显示任何图标
mAlert.setVisibility(View.GONE);
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 格式化创建时间为年月日 上午/下午 HH:mm 格式显示
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault());
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.setTimeInMillis(data.getCreatedDate());
int hour = calendar.get(java.util.Calendar.HOUR_OF_DAY);
String timeStr;
if (hour < 12) {
timeStr = "上午 " + sdf.format(new java.util.Date(data.getCreatedDate()));
} else {
timeStr = "下午 " + sdf.format(new java.util.Date(data.getCreatedDate()));
}
mTime.setText(timeStr);
// 更新副标题
TextView tvSubtitle = (TextView) findViewById(R.id.tv_subtitle);
if (tvSubtitle != null) {
if (data.getType() == Notes.TYPE_NOTE) {
// 对于普通笔记,显示内容摘要
if (!data.isEncrypted()) {
String snippet = data.getSnippet();
if (snippet != null && !snippet.isEmpty()) {
String formattedSnippet = DataUtils.getFormattedSnippet(snippet);
// 如果有标题,且摘要与标题不同,则显示摘要
String title = data.getTitle();
if (title != null && !title.isEmpty() && !formattedSnippet.equals(title)) {
tvSubtitle.setText(formattedSnippet);
tvSubtitle.setVisibility(View.VISIBLE);
} else {
// 如果没有标题或摘要与标题相同,则隐藏副标题
tvSubtitle.setVisibility(View.GONE);
}
} else {
// 如果没有摘要,则隐藏副标题
tvSubtitle.setVisibility(View.GONE);
}
} else {
// 对于加密笔记,隐藏副标题
tvSubtitle.setVisibility(View.GONE);
}
} else {
// 对于文件夹,隐藏副标题
tvSubtitle.setVisibility(View.GONE);
}
}
setBackground(data);
}
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
/**
*
* @param context
* @param alertDate
* @return
*/
private String getRemainingReminderTime(Context context, long alertDate) {
long currentTime = System.currentTimeMillis();
long remainingTime = alertDate - currentTime;
if (remainingTime <= 0) {
return "";
}
// 计算天、小时、分钟
long days = remainingTime / (24 * 60 * 60 * 1000);
long hours = (remainingTime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000);
long minutes = (remainingTime % (60 * 60 * 1000)) / (60 * 1000);
// 格式化输出
if (days > 0) {
return context.getString(R.string.format_reminder_days, days);
} else if (hours > 0) {
return context.getString(R.string.format_reminder_hours, hours);
} else if (minutes > 0) {
return context.getString(R.string.format_reminder_minutes, minutes);
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
return context.getString(R.string.format_reminder_soon);
}
}
private void setBackground(NoteItemData data) {
// 使用白色背景,忽略笔记的背景颜色设置
setBackgroundResource(R.drawable.note_card_bg);
}
public NoteItemData getItemData() {
return mItemData;
}
}
}

@ -61,7 +61,7 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },
new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLDER) },
null);
}

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#F0F0F0" />
<corners android:radius="4dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="#FFF0E6" />
<corners android:radius="4dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FF666666" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="2dp" />
<solid android:color="#FFC107" />
</shape>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z"/>
</vector>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<corners android:radius="8dp" />
</shape>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#000000" />
<corners android:radius="4dp" />
</shape>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#CCCCCC" />
<corners android:radius="20dp" />
</shape>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角 -->
<corners android:radius="22dp" />
<!-- 背景色 -->
<solid android:color="#30FFFFFF" />
<!-- 边框 -->
<stroke
android:width="1dp"
android:color="#10FFFFFF" />
</shape>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<stroke
android:width="2dp"
android:color="#333333"
android:dashWidth="0dp"
android:dashGap="0dp" />
<size
android:width="16dp"
android:height="2dp" />
</shape>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:orientation="vertical">
<!-- 透明布局仅用于确保Activity能够正常创建 -->
<!-- 对话框将通过代码动态显示 -->
</RelativeLayout>

@ -0,0 +1,37 @@
<?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="wrap_content"
android:layout_height="wrap_content"
android:text="开始日期"
android:textSize="16sp"
android:layout_marginBottom="8dp" />
<DatePicker
android:id="@+id/start_date_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:spinnersShown="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="结束日期"
android:textSize="16sp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" />
<DatePicker
android:id="@+id/end_date_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:spinnersShown="true" />
</LinearLayout>

@ -0,0 +1,62 @@
<?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="180dp"
android:background="@drawable/note_card_bg"
android:orientation="vertical"
android:padding="16dp"
android:layout_margin="8dp"
android:elevation="0dp">
<!-- 标题 -->
<TextView
android:id="@+id/tv_card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"
android:maxLines="2"
android:ellipsize="end"
android:text="笔记标题" />
<!-- 无附加文案 -->
<TextView
android:id="@+id/tv_card_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#999999"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:text="无附加文案" />
<!-- 时间戳 -->
<TextView
android:id="@+id/tv_card_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#666666"
android:textSize="12sp"
android:layout_gravity="left|bottom"
android:layout_weight="1"
android:gravity="left|bottom"
android:text="上午 9:55" />
</LinearLayout>

@ -18,7 +18,7 @@
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background"
android:background="#FFFFFF"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
@ -26,10 +26,61 @@
android:layout_height="fill_parent"
android:orientation="vertical">
<!-- 顶部导航栏 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="56dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp">
<!-- 左侧返回按钮 -->
<ImageButton
android:id="@+id/btn_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_revert"
android:tint="#000000" /> <!-- 设置为黑色 -->
<!-- 中间占位 -->
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- 右侧操作按钮 -->
<ImageButton
android:id="@+id/btn_share"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_share"
android:tint="#000000" /> <!-- 设置为黑色 -->
<ImageButton
android:id="@+id/btn_theme"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_manage"
android:tint="#000000" /> <!-- 设置为黑色 -->
<ImageButton
android:id="@+id/btn_more"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@android:color/transparent"
android:src="@android:drawable/ic_menu_more"
android:tint="#000000" /> <!-- 设置为黑色 -->
</LinearLayout>
<!-- 标题栏(隐藏,仅用于代码兼容) -->
<LinearLayout
android:id="@+id/note_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:id="@+id/tv_modified_date"
@ -56,73 +107,167 @@
android:layout_marginRight="8dip"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
<ImageButton
<ImageView
android:id="@+id/btn_set_bg_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_btn_set_color" />
android:layout_gravity="center_vertical" />
</LinearLayout>
<LinearLayout
<!-- 内容区 -->
<ScrollView
android:id="@+id/sv_note_edit"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:padding="0dp"
android:layout_margin="0dp"
android:overScrollMode="never">
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
<ScrollView
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:layout_gravity="left|top"
android:fadingEdgeLength="0dip">
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="0dp"
android:paddingBottom="32dp"
android:layout_margin="0dp"
android:gravity="top"
android:baselineAligned="false">
<!-- 标题输入框 -->
<EditText
android:id="@+id/note_title_view"
android:layout_width="fill_parent"
android:layout_height="50dp"
android:background="@drawable/note_title_border"
android:hint="标题"
android:textColorHint="#666666"
android:singleLine="true"
android:textSize="18sp"
android:textColor="#000000"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:includeFontPadding="false"
android:maxLines="1" />
<!-- 正文内容 -->
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:textColor="#000000"
android:textSize="16sp"
android:lineSpacingMultiplier="1.5"
android:padding="0dp"
android:layout_margin="0dp"
android:includeFontPadding="false"
android:minHeight="100dp" />
<!-- 信息行 -->
<TextView
android:id="@+id/tv_info_line"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="1 月 31 日 上午 9:55 | 0 字"
android:textColor="#999999"
android:textSize="12sp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_margin="0dp"
android:includeFontPadding="false" />
<!-- 笔记编辑列表(隐藏) -->
<LinearLayout
android:id="@+id/note_edit_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="left|top"
android:background="@null"
android:autoLink="all"
android:linksClickable="false"
android:minLines="12"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:lineSpacingMultiplier="1.2" />
<LinearLayout
android:id="@+id/note_edit_list"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="-10dip"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
<ImageView
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
</LinearLayout>
<!-- 底部系统手势条占位 -->
<View
android:layout_width="fill_parent"
android:layout_height="24dp"
android:background="#FFFFFF" />
</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" />
<!-- 富文本编辑工具栏(隐藏) -->
<LinearLayout
android:id="@+id/rich_text_toolbar"
android:layout_width="fill_parent"
android:layout_height="56dp"
android:background="#FFFFFF"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:visibility="gone">
<!-- 加粗按钮 -->
<ImageButton
android:id="@+id/btn_bold"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_format_bold"
android:tint="#666666"
android:contentDescription="加粗"
android:scaleType="center"
android:padding="12dp" />
<!-- 斜体按钮 -->
<ImageButton
android:id="@+id/btn_italic"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_format_italic"
android:tint="#666666"
android:contentDescription="斜体"
android:scaleType="center"
android:padding="12dp" />
<!-- 下划线按钮 -->
<ImageButton
android:id="@+id/btn_underline"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_format_underlined"
android:tint="#666666"
android:contentDescription="下划线"
android:scaleType="center"
android:padding="12dp" />
<!-- 有序列表按钮 -->
<ImageButton
android:id="@+id/btn_number_list"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_format_list_numbered"
android:tint="#666666"
android:contentDescription="有序列表"
android:scaleType="center"
android:padding="12dp" />
<!-- 无序列表按钮 -->
<ImageButton
android:id="@+id/btn_bullet_list"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_format_list_bulleted"
android:tint="#666666"
android:contentDescription="无序列表"
android:scaleType="center"
android:padding="12dp" />
</LinearLayout>
<!-- 背景颜色选择器(隐藏) -->
<LinearLayout
android:id="@+id/note_bg_color_selector"
android:layout_width="wrap_content"
@ -169,9 +314,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dip"
android:src="@drawable/selected" />
</FrameLayout>
@ -190,9 +335,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dip"
android:src="@drawable/selected" />
</FrameLayout>
@ -211,6 +356,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
@ -231,12 +377,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<!-- 字体大小选择器(隐藏) -->
<LinearLayout
android:id="@+id/font_size_selector"
android:layout_width="fill_parent"
@ -316,8 +464,6 @@
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
@ -354,8 +500,6 @@
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
@ -392,9 +536,7 @@
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
</FrameLayout>
</FrameLayout>

@ -23,43 +23,49 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
android:layout_height="180dp"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="0dip"
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="0dip"
android:layout_weight="1"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:visibility="gone" />
android:textAppearance="@style/TextAppearancePrimaryItem"
android:visibility="gone" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<!-- 标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"
android:maxLines="2"
android:ellipsize="end" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true" />
<!-- 无附加文案 -->
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#999999"
android:textSize="12sp"
android:layout_marginBottom="16dp"
android:text="无附加文案" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
</LinearLayout>
</LinearLayout>
<!-- 时间戳 -->
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#666666"
android:textSize="12sp"
android:layout_gravity="left|bottom"
android:layout_weight="1"
android:gravity="left|bottom" />
<CheckBox
android:id="@android:id/checkbox"
@ -68,11 +74,11 @@
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"/>
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"/>
</LinearLayout>
</FrameLayout>

@ -15,44 +15,239 @@
limitations under the License.
-->
<FrameLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<!-- 应用名称 -->
<TextView
android:id="@+id/tv_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="笔记"
android:textColor="#000000"
android:textSize="24sp"
android:textStyle="bold" />
<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" />
<ListView
android:id="@+id/notes_list"
android:layout_width="fill_parent"
android:layout_height="0dip"
<!-- 搜索栏和时间搜索按钮 -->
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@id/tv_app_name"
android:layout_marginTop="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 搜索栏 -->
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_weight="1"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
android:layout_height="40dp"
android:background="@drawable/search_bar_bg"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_menu_search"
android:layout_marginRight="8dp"
android:tint="#999999" />
<EditText
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="36dp"
android:hint="搜索笔记"
android:textColor="#000000"
android:textSize="14sp"
android:background="@null"
android:singleLine="true"
android:imeOptions="actionSearch"
android:cursorVisible="true"
android:textCursorDrawable="@null"
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true" />
</LinearLayout>
<!-- 按时间查找按钮 -->
<ImageView
android:id="@+id/btn_time_search"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="12dp"
android:src="@android:drawable/ic_menu_today"
android:tint="#000000"
android:clickable="true"
android:focusable="true" />
<!-- 排序按钮 -->
<ImageView
android:id="@+id/btn_sort"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="12dp"
android:src="@android:drawable/ic_menu_sort_by_size"
android:tint="#000000"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<Button
android:id="@+id/btn_new_note"
android:background="@drawable/new_note"
<!-- 删除按钮容器 -->
<LinearLayout
android:id="@+id/delete_button_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:layout_gravity="bottom" />
</FrameLayout>
android:layout_height="48dp"
android:background="#FFFFFF"
android:orientation="horizontal"
android:gravity="center"
android:layout_alignParentBottom="true"
android:visibility="gone">
<!-- 删除按钮 -->
<Button
android:id="@+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:background="#FF4444"
android:textColor="#FFFFFF"
android:text="删除"
android:textSize="14sp"
android:paddingLeft="32dp"
android:paddingRight="32dp"
android:visibility="gone" />
</LinearLayout>
<!-- 底部导航栏 -->
<LinearLayout
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#FFFFFF"
android:orientation="horizontal"
android:gravity="center"
android:layout_alignParentBottom="true"
android:layout_above="@+id/delete_button_container">
<!-- 笔记标签 -->
<LinearLayout
android:id="@+id/nav_notes"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_gallery"
android:layout_marginBottom="4dp"
android:tint="#000000" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="笔记"
android:textColor="#000000"
android:textSize="12sp" />
</LinearLayout>
<!-- 待办标签 -->
<LinearLayout
android:id="@+id/nav_todo"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_myplaces"
android:layout_marginBottom="4dp"
android:tint="#CCCCCC" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="待办"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
<!-- 回收站标签 -->
<LinearLayout
android:id="@+id/nav_recycle"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_delete"
android:layout_marginBottom="4dp"
android:tint="#CCCCCC" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="回收站"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
<!-- 笔记卡片网格布局 -->
<GridView
android:id="@+id/notes_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/search_container"
android:layout_above="@+id/bottom_nav"
android:numColumns="2"
android:columnWidth="0dp"
android:stretchMode="columnWidth"
android:horizontalSpacing="16dp"
android:verticalSpacing="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="80dp" />
<!-- 悬浮新建按钮 -->
<LinearLayout
android:id="@+id/fab_new_note"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="#FFC107"
android:gravity="center"
android:layout_marginRight="24dp"
android:layout_marginBottom="80dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:clickable="true"
android:focusable="true"
android:elevation="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</RelativeLayout>

@ -0,0 +1,199 @@
<?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.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<!-- 页面标题 -->
<TextView
android:id="@+id/tv_app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="待办"
android:textColor="#000000"
android:textSize="24sp"
android:textStyle="bold" />
<!-- 待办事项列表 -->
<LinearLayout
android:id="@+id/todo_list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/tv_app_name"
android:layout_above="@+id/bottom_nav"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<!-- 已完成列表折叠区域 -->
<LinearLayout
android:id="@+id/completed_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true">
<!-- 向上的小箭头图标 -->
<ImageView
android:id="@+id/completed_arrow"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_menu_more"
android:layout_marginRight="8dp"
android:tint="#000000" />
<!-- 已完成标题 -->
<TextView
android:id="@+id/completed_title"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="已完成 0"
android:textColor="#000000"
android:textSize="14sp" />
</LinearLayout>
<!-- 已完成列表 -->
<LinearLayout
android:id="@+id/completed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
</LinearLayout>
</LinearLayout>
<!-- 黄色圆形悬浮按钮 -->
<LinearLayout
android:id="@+id/fab_new_note"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="#FFC107"
android:gravity="center"
android:layout_marginRight="24dp"
android:layout_marginBottom="80dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:clickable="true"
android:focusable="true"
android:elevation="4dp"
android:radius="28dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<!-- 底部导航栏 -->
<LinearLayout
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#FFFFFF"
android:orientation="horizontal"
android:gravity="center"
android:layout_alignParentBottom="true">
<!-- 笔记标签 -->
<LinearLayout
android:id="@+id/nav_notes"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_gallery"
android:layout_marginBottom="4dp"
android:tint="#000000" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="笔记"
android:textColor="#000000"
android:textSize="12sp" />
</LinearLayout>
<!-- 待办标签 -->
<LinearLayout
android:id="@+id/nav_todo"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_myplaces"
android:layout_marginBottom="4dp"
android:tint="#CCCCCC" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="待办"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
<!-- 回收站标签 -->
<LinearLayout
android:id="@+id/nav_recycle"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_delete"
android:layout_marginBottom="4dp"
android:tint="#CCCCCC" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="回收站"
android:textColor="#CCCCCC"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

@ -18,6 +18,18 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_pin"
android:title="@string/menu_pin" />
<item
android:id="@+id/menu_encrypt"
android:title="@string/menu_encrypt" />
<item
android:id="@+id/menu_decrypt"
android:title="@string/menu_decrypt" />
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
@ -29,6 +41,10 @@
<item
android:id="@+id/menu_font_size"
android:title="@string/menu_font_size"/>
<item
android:id="@+id/menu_rich_text"
android:title="@string/menu_rich_text" />
<item
android:id="@+id/menu_list_mode"
@ -49,4 +65,10 @@
<item
android:id="@+id/menu_delete_remind"
android:title="@string/menu_remove_remind" />
<item
android:id="@+id/menu_insert_image"
android:title="插入图片" />
</menu>

@ -21,6 +21,8 @@
android:id="@+id/menu_new_folder"
android:title="@string/menu_create_folder"/>
<item
android:id="@+id/menu_export_text"
android:title="@string/menu_export_text"/>

@ -29,6 +29,7 @@
<string name="format_datetime_mdhm">MM月dd日 kk:mm</string>
<string name="notealert_ok">知道了</string>
<string name="notealert_enter">查看</string>
<string name="notealert_no_content">无内容</string>
<string name="note_link_tel">呼叫电话</string>
<string name="note_link_email">发送邮件</string>
<string name="note_link_web">浏览网页</string>
@ -115,5 +116,21 @@
<string name="search_setting_description">便签中的文字</string>
<string name="datetime_dialog_ok">设置</string>
<string name="datetime_dialog_cancel">取消</string>
<!-- 提醒时间格式化字符串 -->
<string name="format_reminder_days">%d天后</string>
<string name="format_reminder_hours">%d小时后</string>
<string name="format_reminder_minutes">%d分钟后</string>
<string name="format_reminder_soon">即将提醒</string>
<!-- 最近删除功能 -->
<string name="title_recently_deleted">最近删除</string>
<string name="menu_restore">恢复</string>
<string name="menu_delete_permanently">彻底删除</string>
<string name="alert_message_delete_permanently">确认要彻底删除所选的 %d 条便签吗?删除后将无法恢复。</string>
<string name="toast_restore_success">已成功恢复所选便签</string>
<!-- 提醒功能相关字符串 -->
<string name="error_alert_time_invalid">提醒时间必须是未来时间</string>
<string name="toast_alert_set_success">提醒设置成功</string>
<string name="toast_alert_cancel_success">提醒取消成功</string>
<string name="error_note_save_failed">便签保存失败</string>
</resources>

@ -17,110 +17,155 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">Notes</string>
<string name="app_widget2x2">Notes 2x2</string>
<string name="app_widget4x4">Notes 4x4</string>
<string name="widget_havenot_content">No associated note found, click to create associated note.</string>
<string name="widget_under_visit_mode">Privacy modecan not see note content</string>
<string name="app_name">便签</string>
<string name="app_widget2x2">便签 2x2</string>
<string name="app_widget4x4">便签 4x4</string>
<string name="widget_havenot_content">未找到关联便签,点击创建关联便签。</string>
<string name="widget_under_visit_mode">隐私模式,无法查看便签内容</string>
<string name="notelist_string_info">...</string>
<string name="notelist_menu_new">Add note</string>
<string name="note_alert_expired">Expired</string>
<string name="notelist_menu_new">添加便签</string>
<string name="note_alert_expired">已过期</string>
<string name="format_date_ymd">yyyyMMdd</string>
<string name="format_datetime_mdhm">MMMd kk:mm</string>
<string name="notealert_ok">Got it</string>
<string name="notealert_enter">Take a look</string>
<string name="note_link_tel">Call</string>
<string name="note_link_email">Send email</string>
<string name="note_link_web">Browse web</string>
<string name="note_link_other">Open map</string>
<string name="notealert_ok">知道了</string>
<string name="notealert_enter">查看</string>
<string name="notealert_no_content">无内容</string>
<string name="note_link_tel">拨打电话</string>
<string name="note_link_email">发送邮件</string>
<string name="note_link_web">浏览网页</string>
<string name="note_link_other">打开地图</string>
<!-- Text export file information -->
<string name="file_path">/MIUI/notes/</string>
<string name="file_name_txt_format">notes_%s.txt</string>
<!-- notes list string -->
<string name="format_folder_files_count">(%d)</string>
<string name="menu_create_folder">New Folder</string>
<string name="menu_export_text">Export text</string>
<string name="menu_sync">Sync</string>
<string name="menu_sync_cancel">Cancel syncing</string>
<string name="menu_setting">Settings</string>
<string name="menu_search">Search</string>
<string name="menu_delete">Delete</string>
<string name="menu_move">Move to folder</string>
<string name="menu_select_title">%d selected</string>
<string name="menu_select_none">Nothing selected, the operation is invalid</string>
<string name="menu_select_all">Select all</string>
<string name="menu_deselect_all">Deselect all</string>
<string name="menu_font_size">Font size</string>
<string name="menu_font_small">Small</string>
<string name="menu_font_normal">Medium</string>
<string name="menu_font_large">Large</string>
<string name="menu_font_super">Super</string>
<string name="menu_list_mode">Enter check list</string>
<string name="menu_normal_mode">Leave check list</string>
<string name="menu_folder_view">View folder</string>
<string name="menu_folder_delete">Delete folder</string>
<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_send_to_desktop">Send to home</string>
<string name="menu_alert">Remind me</string>
<string name="menu_remove_remind">Delete reminder</string>
<string name="menu_title_select_folder">Select folder</string>
<string name="menu_move_parent_folder">Parent folder</string>
<string name="info_note_enter_desktop">Note added to home</string>
<string name="alert_message_delete_folder">Confirm to delete folder and its notes?</string>
<string name="alert_title_delete">Delete selected notes</string>
<string name="alert_message_delete_notes">Confirm to delete the selected %d notes?</string>
<string name="alert_message_delete_note">Confirm to delete this note?</string>
<string name="format_move_notes_to_folder">Have moved selected %1$d notes to %2$s folder</string>
<string name="menu_create_folder">新建文件夹</string>
<string name="menu_export_text">导出文本</string>
<string name="menu_sync">同步</string>
<string name="menu_sync_cancel">取消同步</string>
<string name="menu_setting">设置</string>
<string name="menu_search">搜索</string>
<string name="menu_delete">删除</string>
<string name="menu_move">移动到文件夹</string>
<string name="menu_select_title">已选择 %d 项</string>
<string name="menu_select_none">未选择任何项,操作无效</string>
<string name="menu_select_all">全选</string>
<string name="menu_deselect_all">取消全选</string>
<string name="menu_font_size">字体大小</string>
<string name="menu_font_small"></string>
<string name="menu_font_normal"></string>
<string name="menu_font_large"></string>
<string name="menu_font_super">超大</string>
<string name="menu_rich_text">富文本编辑器</string>
<string name="menu_rich_text_hide">隐藏富文本编辑器</string>
<string name="menu_list_mode">进入检查列表</string>
<string name="menu_normal_mode">退出检查列表</string>
<string name="menu_folder_view">查看文件夹</string>
<string name="menu_folder_delete">删除文件夹</string>
<string name="menu_folder_change_name">修改文件夹名称</string>
<string name="folder_exist">文件夹 %1$s 已存在,请重命名</string>
<string name="menu_share">分享</string>
<string name="menu_send_to_desktop">发送到桌面</string>
<string name="menu_alert">提醒我</string>
<string name="menu_remove_remind">删除提醒</string>
<string name="menu_title_select_folder">选择文件夹</string>
<string name="menu_move_parent_folder">父文件夹</string>
<string name="info_note_enter_desktop">便签已添加到桌面</string>
<string name="alert_message_delete_folder">确认删除文件夹及其便签?</string>
<string name="alert_title_delete">删除选中的便签</string>
<string name="alert_message_delete_notes">确认删除选中的 %d 个便签?</string>
<string name="alert_message_delete_note">确认删除此便签?</string>
<string name="format_move_notes_to_folder">已将选中的 %1$d 个便签移动到 %2$s 文件夹</string>
<!-- Error information -->
<string name="error_sdcard_unmounted">SD card busy, not available now</string>
<string name="error_sdcard_export">Export failed, please check SD card</string>
<string name="error_note_not_exist">The note is not exist</string>
<string name="error_note_empty_for_clock">Sorry, can not set clock on empty note</string>
<string name="error_note_empty_for_send_to_desktop">Sorry, can not send and empty note to home</string>
<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>
<string name="error_sdcard_unmounted">SD卡忙,暂时不可用</string>
<string name="error_sdcard_export">导出失败请检查SD卡</string>
<string name="error_note_not_exist">便签不存在</string>
<string name="error_note_empty_for_clock">抱歉,无法为空白便签设置提醒</string>
<string name="error_note_empty_for_send_to_desktop">抱歉,无法将空白便签发送到桌面</string>
<string name="success_sdcard_export">导出成功</string>
<string name="failed_sdcard_export">导出失败</string>
<string name="format_exported_file_location">已将文本文件 (%1$s) 导出到SD卡 (%2$s) 目录</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>
<string name="ticker_syncing">正在同步便签...</string>
<string name="ticker_success">同步成功</string>
<string name="ticker_fail">同步失败</string>
<string name="ticker_cancel">同步已取消</string>
<string name="success_sync_account">与账户 %1$s 同步成功</string>
<string name="error_sync_network">同步失败,请检查网络和账户设置</string>
<string name="error_sync_internal">同步失败,发生内部错误</string>
<string name="error_sync_cancelled">同步已取消</string>
<string name="sync_progress_login">正在登录 %1$s...</string>
<string name="sync_progress_init_list">正在获取远程便签列表...</string>
<string name="sync_progress_syncing">正在将本地便签与Google任务同步...</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_title">设置</string>
<string name="preferences_account_title">同步账户</string>
<string name="preferences_account_summary">将便签与Google任务同步</string>
<string name="preferences_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_bg_random_appear_title">New note background color random</string>
<string name="preferences_add_account">添加账户</string>
<string name="preferences_menu_change_account">更改同步账户</string>
<string name="preferences_menu_remove_account">移除同步账户</string>
<string name="preferences_menu_cancel">取消</string>
<string name="preferences_button_sync_immediately">立即同步</string>
<string name="preferences_button_sync_cancel">取消同步</string>
<string name="preferences_dialog_change_account_title">当前账户 %1$s</string>
<string name="preferences_dialog_change_account_warn_msg">所有同步相关信息将被删除,可能会导致有时出现重复项目</string>
<string name="preferences_dialog_select_account_title">同步便签</string>
<string name="preferences_dialog_select_account_tips">请选择一个Google账户。本地便签将与Google任务同步。</string>
<string name="preferences_toast_cannot_change_account">无法更改账户,因为同步正在进行中</string>
<string name="preferences_toast_success_set_accout">%1$s 已设置为同步账户</string>
<string name="preferences_bg_random_appear_title">新便签背景颜色随机</string>
<string name="call_record_folder_name">通话记录</string>
<string name="hint_foler_name">输入名称</string>
<string name="search_label">搜索便签</string>
<string name="search_hint">搜索便签</string>
<string name="search_setting_description">便签中的文本</string>
<string name="dialog_search">搜索</string>
<string name="dialog_enter_search_query">输入搜索内容</string>
<string name="dialog_clear_search">清除搜索</string>
<string name="datetime_dialog_ok">设置</string>
<string name="datetime_dialog_cancel">取消</string>
<!-- Pin note -->
<string name="menu_pin">置顶</string>
<string name="menu_unpin">取消置顶</string>
<!-- Encrypt note -->
<string name="menu_encrypt">加密</string>
<string name="menu_decrypt">解密</string>
<string name="dialog_enter_password">输入密码</string>
<string name="dialog_set_password">设置密码</string>
<string name="dialog_confirm_password">确认密码</string>
<string name="dialog_password_mismatch">密码不匹配</string>
<string name="dialog_password_empty">密码不能为空</string>
<string name="toast_encrypt_success">便签加密成功</string>
<string name="toast_decrypt_success">便签解密成功</string>
<string name="toast_password_incorrect">密码错误</string>
<!-- Recently deleted -->
<string name="menu_recently_deleted">最近删除</string>
<string name="title_recently_deleted">最近删除</string>
<string name="empty_recently_deleted">无最近删除的便签</string>
<string name="menu_restore">恢复</string>
<string name="menu_delete_permanently">永久删除</string>
<string name="alert_message_delete_permanently">确认永久删除选中的 %d 个便签?</string>
<string name="toast_restore_success">便签恢复成功</string>
<string name="toast_delete_permanently_success">便签永久删除成功</string>
<!-- Reminder strings -->
<string name="error_alert_time_invalid">提醒时间必须在未来</string>
<string name="toast_alert_set_success">提醒设置成功</string>
<string name="toast_alert_cancel_success">提醒取消成功</string>
<string name="toast_delete_success">便签删除成功</string>
<string name="error_note_save_failed">保存便签失败</string>
<string name="format_reminder_days">%d天后</string>
<string name="format_reminder_hours">%d小时后</string>
<string name="format_reminder_minutes">%d分钟后</string>
<string name="format_reminder_soon">即将提醒</string>
<string name="call_record_folder_name">Call notes</string>
<string name="hint_foler_name">Input name</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>
<string name="datetime_dialog_ok">set</string>
<string name="datetime_dialog_cancel">cancel</string>
</resources>

Loading…
Cancel
Save