Compare commits

..

1 Commits

Author SHA1 Message Date
林迪文 87e1516229 插入图片
1 month ago

3
.idea/.gitignore vendored

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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$/MiCode.iml" filepath="$PROJECT_DIR$/MiCode.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" 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$/src/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

24
src/.gitignore vendored

@ -1,15 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
project.properties
.settings/
.classpath
.project

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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$/MiCode.iml" filepath="$PROJECT_DIR$/MiCode.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,14 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 悬浮窗权限 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 前台服务权限 (Android 9+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 允许查询窗口状态 (可选,为了更好适配) -->
<uses-permission android:name="android.permission.GET_TASKS"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
@ -18,16 +10,12 @@
<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.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 原有的权限可能已经存在,请确保补充以下权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 针对 Android 13+ (API 33+) 的图片读取权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- 通知权限:便签时间提醒需在设定时间弹出通知 (Android 13+ 需在应用信息中开启) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 声明相机硬件特性(非强制,但推荐) -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
@ -38,24 +26,17 @@
android:theme="@style/NoteTheme"
tools:targetApi="31">
<!-- 开场动画:作为启动入口 -->
<!-- 新的入口 -->
<activity
android:name=".ui.SplashActivity"
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait">
android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <!-- 注意 Theme -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 主界面:由 SplashActivity 跳转 -->
<activity
android:name=".ui.MainActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- 旧的 Activity保留声明去掉 intent-filter -->
<activity
android:name=".ui.NotesListActivity"
@ -107,11 +88,7 @@
android:resource="@xml/searchable" />
</activity>
<service
android:name=".tool.FloatingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
@ -158,10 +135,8 @@
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" />
<receiver
android:name=".ui.NoteReminderReceiver"
android:exported="false" />
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
@ -170,19 +145,6 @@
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name=".ui.TodoEditActivity"
android:exported="false"
android:theme="@style/TodoEditTheme" />
<receiver
android:name=".todo.TodoAlarmReceiver"
android:exported="false" />
<receiver
android:name=".todo.TodoGeofenceReceiver"
android:exported="false" />
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"

@ -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$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,190 @@
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.
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.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

@ -0,0 +1,23 @@
[中文]
1. MiCode便签是小米便签的社区开源版由MIUI团队(www.miui.com) 发起并贡献第一批代码遵循NOTICE文件所描述的开源协议
今后为MiCode社区(www.micode.net) 拥有,并由社区发布和维护。
2. Bug反馈和跟踪请访问Github,
https://github.com/MiCode/Notes/issues?sort=created&direction=desc&state=open
3. 功能建议和综合讨论请访问MiCode,
http://micode.net/forum.php?mod=forumdisplay&fid=38
[English]
1. MiCode Notes is open source edition of XM notepad, it's first initiated and sponsored by MIUI team (www.miui.com).
It's opened under license described by NOTICE file. It's owned by the MiCode community (www.micode.net). In future,
the MiCode community will release and maintain this project.
2. Regarding issue tracking, please visit Github,
https://github.com/MiCode/Notes/issues?sort=created&direction=desc&state=open
3. Regarding feature request and general discussion, please visit Micode forum,
http://micode.net/forum.php?mod=forumdisplay&fid=38

@ -1 +0,0 @@
/build

@ -1,79 +0,0 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "net.micode.notes"
// 修改这里:匹配你下载的 SDK 30
compileSdk = 34
defaultConfig {
applicationId = "net.micode.notes"
minSdk = 30
// 修改这里:匹配你下载的 SDK 30
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// === 这里是新增的代码,用来解决 Jar 包冲突报错 ===
packaging {
resources {
excludes += "META-INF/DEPENDENCIES"
excludes += "META-INF/NOTICE"
excludes += "META-INF/LICENSE"
excludes += "META-INF/LICENSE.txt"
excludes += "META-INF/NOTICE.txt"
}
}
// ==============================================
}
dependencies {
// === 修改部分:使用兼容 SDK 30 的旧版本库 ===
// 替换掉了原本的 libs.appcompat 等引用,改为写死的旧版本号
// ViewPager2 用于左右滑动
implementation("androidx.recyclerview:recyclerview:1.3.0")
implementation("androidx.viewpager2:viewpager2:1.0.0")
// Material Design 用于底部导航栏
implementation("com.google.android.material:material:1.9.0")
// Fragment 核心库 (确保使用 AndroidX 版本)
implementation("androidx.fragment:fragment:1.5.7")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
// 如果你需要 Activity 的新特性,也需要指定旧版本,否则默认会拉取最新的导致报错
implementation("androidx.activity:activity:1.2.4")
// Google Play Services - 用于地理围栏
implementation("com.google.android.gms:play-services-location:21.0.1")
// === 保持不变:你的 Jar 包路径 ===
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
// === 测试库也建议指定旧版本,防止冲突 ===
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
}

Binary file not shown.

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -1,26 +0,0 @@
package net.micode.notes;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("net.micode.notes", appContext.getPackageName());
}
}

@ -1,148 +0,0 @@
package net.micode.notes.ai;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import java.util.ArrayList;
import java.util.List;
/**
* (Retrieval Layer)
* 便
*/
public class NoteRetriever {
public static class RetrievedNote {
public long id;
public String title;
public String content;
public long modifiedDate;
public RetrievedNote(long id, String title, String content, long date) {
this.id = id;
this.title = title;
this.content = content;
this.modifiedDate = date;
}
}
/**
*
* @param query
* @return 便
*/
public static List<RetrievedNote> searchRelevantNotes(Context context, String query) {
List<RetrievedNote> results = new ArrayList<>();
// 1. 尝试关键词搜索
// ... (保留你之前的代码逻辑) ...
// ---【修改开始:在这里插入逻辑】---
// ... (执行原本的 SQL 查询) ...
// 关键修改点:如果关键词搜索结果为空,或者用户只发了简单的问候
if (results.isEmpty()) {
Log.d("NoteRetriever", "关键词搜索无果,启动兜底策略:获取最近 10 条便签");
results.addAll(getRecentNotes(context, 10));
} else {
Log.d("NoteRetriever", "关键词搜索成功,找到 " + results.size() + " 条");
}
return results;
}
// 【新增方法】获取最近的 N 条便签
private static List<RetrievedNote> getRecentNotes(Context context, int limit) {
List<RetrievedNote> list = new ArrayList<>();
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
Notes.CONTENT_NOTE_URI, // 直接查 Note 主表
new String[]{NoteColumns.ID, NoteColumns.SNIPPET, NoteColumns.MODIFIED_DATE},
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLER)},
NoteColumns.MODIFIED_DATE + " DESC LIMIT " + limit // 按时间倒序,取前 N 条
);
if (cursor != null && cursor.moveToFirst()) {
do {
long id = cursor.getLong(0);
String snippet = cursor.getString(1); // 标题
long date = cursor.getLong(2);
// 再去 Data 表查这一条的完整内容
String content = getNoteContent(context, id);
// 如果内容为空,用标题代替
if (TextUtils.isEmpty(content)) content = snippet;
list.add(new RetrievedNote(id, snippet, content, date));
} while (cursor.moveToNext());
}
} catch (Exception e) {
Log.e("NoteRetriever", "Get recent notes failed", e);
} finally {
if (cursor != null) cursor.close();
}
return list;
}
// 【新增辅助方法】获取单条便签的内容
private static String getNoteContent(Context context, long noteId) {
String content = "";
Cursor c = null;
try {
c = context.getContentResolver().query(
Notes.CONTENT_DATA_URI,
new String[]{DataColumns.CONTENT},
DataColumns.NOTE_ID + "=? AND " + DataColumns.MIME_TYPE + "=?",
new String[]{String.valueOf(noteId), Notes.TextNote.CONTENT_ITEM_TYPE},
null
);
if (c != null && c.moveToFirst()) {
content = c.getString(0);
}
} finally {
if (c != null) c.close();
}
return content;
}
// 辅助方法:获取标题
private static String getSnippet(Context context, long noteId) {
String snippet = "无标题";
Cursor c = context.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.SNIPPET},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null);
if (c != null) {
if (c.moveToFirst()) snippet = c.getString(0);
c.close();
}
return snippet;
}
// 辅助方法:获取时间
private static long getDate(Context context, long noteId) {
long date = 0;
Cursor c = context.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.MODIFIED_DATE},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null);
if (c != null) {
if (c.moveToFirst()) date = c.getLong(0);
c.close();
}
return date;
}
}

@ -1,65 +0,0 @@
package net.micode.notes.ai;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* AI
* <p>
* DeepSeek
* <ul>
* <li> AI 便</li>
* <li> (RAG)便便</li>
* </ul>
* </p>
*
* @see net.micode.notes.ai.NoteRetriever
* @see net.micode.notes.ai.AIService
*/
public class PromptBuilder {
/**
*
* @param relatedNotes 便
* @param useRAG RAG
*/
public static String buildSystemPrompt(List<NoteRetriever.RetrievedNote> relatedNotes, boolean useRAG) {
// === 分支 1通用 AI 模式 (未勾选知识库) ===
if (!useRAG) {
return "你是一个智能且博学的 AI 助手。请根据用户的提问直接给出准确、有帮助的回答。不需要参考任何本地便签信息。";
}
// === 分支 2私人管家模式 (勾选知识库) ===
StringBuilder sb = new StringBuilder();
// 1. 身份定义与当前环境
String currentTime = new SimpleDateFormat("yyyy年MM月dd日 EEEE HH:mm", Locale.CHINA).format(new Date());
sb.append("你是一个基于用户个人便签的私人管家 'Mi管家'。\n");
sb.append("当前时间是:").append(currentTime).append("。\n");
sb.append("你需要根据用户提供的【参考便签】来回答用户的问题。\n\n");
// 2. 注入参考资料
if (relatedNotes != null && !relatedNotes.isEmpty()) {
sb.append("【参考便签】:\n");
for (NoteRetriever.RetrievedNote note : relatedNotes) {
sb.append("[RefID:").append(note.id).append("] ");
sb.append("标题:").append(note.title).append("\n");
sb.append("内容:").append(note.content).append("\n");
sb.append("修改时间:").append(new SimpleDateFormat("MM-dd", Locale.CHINA).format(new Date(note.modifiedDate))).append("\n");
sb.append("----------------\n");
}
} else {
sb.append("【参考便签】:(无相关记录)\n");
}
// 3. 回答约束 (严苛模式)
sb.append("\n【回答要求】\n");
sb.append("1. 请根据当前时间和参考便签进行推理。\n");
sb.append("2. 如果在参考便签中找到了答案,请简明扼要地回答,并**必须**在句末标注来源,格式为 [Ref:便签标题]。\n");
sb.append("3. 如果参考便签中没有相关信息,请诚实回答“我在您的便签中没找到相关记录”,不要编造。\n");
return sb.toString();
}
}

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

@ -1,153 +0,0 @@
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.todo.TodoItem;
import java.util.ArrayList;
import java.util.List;
/**
* 访 (CRUD)
*/
public class TodoDao {
private static final String TAG = "TodoDao";
private static final String COL_ID = "id";
private static final String COL_CONTENT = "content";
private static final String COL_IS_DONE = "is_done";
private static final String COL_REMINDER_TYPE = "reminder_type";
private static final String COL_REMINDER_TIMESTAMP = "reminder_timestamp";
private static final String COL_LATITUDE = "latitude";
private static final String COL_LONGITUDE = "longitude";
private static final String COL_LOCATION_NAME = "location_name";
private static final String COL_CREATED_TIME = "created_time";
private final NotesDatabaseHelper mHelper;
public TodoDao(Context context) {
mHelper = NotesDatabaseHelper.getInstance(context);
}
public long insert(TodoItem item) {
SQLiteDatabase db = mHelper.getWritableDatabase();
ContentValues cv = toContentValues(item);
long id = db.insert(TABLE.TODO_ITEMS, null, cv);
Log.d(TAG, "insert todo id=" + id);
return id;
}
public int update(TodoItem item) {
if (item.getId() <= 0) return 0;
SQLiteDatabase db = mHelper.getWritableDatabase();
ContentValues cv = toContentValues(item);
return db.update(TABLE.TODO_ITEMS, cv, COL_ID + "=?", new String[]{String.valueOf(item.getId())});
}
public int delete(long id) {
SQLiteDatabase db = mHelper.getWritableDatabase();
return db.delete(TABLE.TODO_ITEMS, COL_ID + "=?", new String[]{String.valueOf(id)});
}
public int deleteByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) return 0;
SQLiteDatabase db = mHelper.getWritableDatabase();
StringBuilder sb = new StringBuilder(COL_ID).append(" IN (");
for (int i = 0; i < ids.size(); i++) {
sb.append(i > 0 ? "," : "").append(ids.get(i));
}
sb.append(")");
return db.delete(TABLE.TODO_ITEMS, sb.toString(), null);
}
public TodoItem getById(long id) {
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_ID + "=?", new String[]{String.valueOf(id)}, null, null, null)) {
if (c != null && c.moveToFirst()) {
return fromCursor(c);
}
}
return null;
}
public List<TodoItem> getAll() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, null, null, null, null, COL_IS_DONE + " ASC, " + COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public List<TodoItem> getUndone() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_IS_DONE + "=0", null, null, null, COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public List<TodoItem> getDone() {
List<TodoItem> list = new ArrayList<>();
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.query(TABLE.TODO_ITEMS, null, COL_IS_DONE + "=1", null, null, null, COL_CREATED_TIME + " DESC")) {
if (c != null) {
while (c.moveToNext()) {
list.add(fromCursor(c));
}
}
}
return list;
}
public int getUndoneCount() {
SQLiteDatabase db = mHelper.getReadableDatabase();
try (Cursor c = db.rawQuery("SELECT COUNT(*) FROM " + TABLE.TODO_ITEMS + " WHERE " + COL_IS_DONE + "=0", null)) {
if (c != null && c.moveToFirst()) {
return c.getInt(0);
}
}
return 0;
}
private ContentValues toContentValues(TodoItem item) {
ContentValues cv = new ContentValues();
cv.put(COL_CONTENT, item.getContent());
cv.put(COL_IS_DONE, item.isDone() ? 1 : 0);
cv.put(COL_REMINDER_TYPE, item.getReminderType());
cv.put(COL_REMINDER_TIMESTAMP, item.getReminderTimestamp());
cv.put(COL_LATITUDE, item.getLatitude());
cv.put(COL_LONGITUDE, item.getLongitude());
cv.put(COL_LOCATION_NAME, item.getLocationName());
cv.put(COL_CREATED_TIME, item.getCreatedTime());
return cv;
}
private TodoItem fromCursor(Cursor c) {
TodoItem item = new TodoItem();
item.setId(c.getLong(c.getColumnIndexOrThrow(COL_ID)));
item.setContent(c.getString(c.getColumnIndexOrThrow(COL_CONTENT)));
item.setDone(c.getInt(c.getColumnIndexOrThrow(COL_IS_DONE)) == 1);
item.setReminderType(c.getInt(c.getColumnIndexOrThrow(COL_REMINDER_TYPE)));
item.setReminderTimestamp(c.getLong(c.getColumnIndexOrThrow(COL_REMINDER_TIMESTAMP)));
item.setLatitude(c.getDouble(c.getColumnIndexOrThrow(COL_LATITUDE)));
item.setLongitude(c.getDouble(c.getColumnIndexOrThrow(COL_LONGITUDE)));
item.setLocationName(c.getString(c.getColumnIndexOrThrow(COL_LOCATION_NAME)));
item.setCreatedTime(c.getLong(c.getColumnIndexOrThrow(COL_CREATED_TIME)));
return item;
}
}

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

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

@ -1,43 +0,0 @@
package net.micode.notes.model;
import java.util.List;
/**
*
* <p>
* Mi AI AI
* AI
* </p>
*
* @see net.micode.notes.ui.ChatAdapter
*/
public class ChatMessage {
public static final int TYPE_USER = 0;
public static final int TYPE_AI = 1;
private int type;
private String content;
private boolean isThinking; // 是否正在思考(仅 AI 有效)
private List<String> references; // 参考资料标题列表(仅 AI 有效)
// 用户消息构造
public ChatMessage(String content) {
this.type = TYPE_USER;
this.content = content;
}
// AI 消息构造
public ChatMessage() {
this.type = TYPE_AI;
this.content = "";
this.isThinking = true; // 默认一开始在思考
}
public int getType() { return type; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public boolean isThinking() { return isThinking; }
public void setThinking(boolean thinking) { isThinking = thinking; }
public List<String> getReferences() { return references; }
public void setReferences(List<String> references) { this.references = references; }
}

@ -1,130 +0,0 @@
package net.micode.notes.todo;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import java.util.ArrayList;
import java.util.Random;
/**
* View Canvas
*/
public class ConfettiView extends View {
/** 粒子数量,铺满整屏 */
private static final int PARTICLE_COUNT = 260;
private static final long DURATION_MS = 2800;
private final ArrayList<Particle> mParticles = new ArrayList<>();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Random mRandom = new Random();
private long mStartTime;
private static class Particle {
float x, y;
float vx, vy;
float size;
int color;
float rotation;
float rotationSpeed;
}
public ConfettiView(Context context) {
super(context);
init();
}
public ConfettiView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint.setStyle(Paint.Style.FILL);
}
public void start() {
mParticles.clear();
setVisibility(VISIBLE);
// 先设为 VISIBLE 再 post确保布局后 getWidth/getHeight 有值GONE 时宽高为 0 会直接 return
post(this::initParticlesIfSized);
}
private void initParticlesIfSized() {
initParticlesIfSized(0);
}
private static final int MAX_LAYOUT_RETRY = 3;
private void initParticlesIfSized(int retryCount) {
int w = getWidth();
int h = getHeight();
if (w <= 0 || h <= 0) {
if (retryCount < MAX_LAYOUT_RETRY) {
requestLayout();
post(() -> initParticlesIfSized(retryCount + 1));
}
return;
}
int[] colors = {
Color.parseColor("#FF6B6B"),
Color.parseColor("#4ECDC4"),
Color.parseColor("#FFE66D"),
Color.parseColor("#95E1D3"),
Color.parseColor("#F38181"),
Color.parseColor("#AA96DA"),
Color.parseColor("#FCBAD3"),
Color.parseColor("#A8D8EA"),
};
for (int i = 0; i < PARTICLE_COUNT; i++) {
Particle p = new Particle();
p.x = (mRandom.nextFloat() - 0.1f) * (w * 1.2f);
p.y = -mRandom.nextFloat() * h * 0.6f;
p.vx = (mRandom.nextFloat() - 0.5f) * 10;
p.vy = 5 + mRandom.nextFloat() * 14;
p.size = 8 + mRandom.nextFloat() * 12;
p.color = colors[mRandom.nextInt(colors.length)];
p.rotation = mRandom.nextFloat() * 360;
p.rotationSpeed = (mRandom.nextFloat() - 0.5f) * 24;
mParticles.add(p);
}
mStartTime = System.currentTimeMillis();
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
if (mParticles.isEmpty()) return;
long elapsed = System.currentTimeMillis() - mStartTime;
if (elapsed >= DURATION_MS) {
setVisibility(GONE);
return;
}
float progress = (float) elapsed / DURATION_MS;
AccelerateInterpolator interpolator = new AccelerateInterpolator();
float alpha = 1f - interpolator.getInterpolation(progress);
for (Particle p : mParticles) {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.3f;
p.rotation += p.rotationSpeed;
mPaint.setColor(p.color);
mPaint.setAlpha((int) (255 * alpha));
canvas.save();
canvas.translate(p.x, p.y);
canvas.rotate(p.rotation);
canvas.drawRect(-p.size / 2, -p.size / 4, p.size / 2, p.size / 4, mPaint);
canvas.restore();
}
invalidate();
}
}

@ -1,251 +0,0 @@
package net.micode.notes.todo;
import android.graphics.Paint;
import android.view.LayoutInflater;
import androidx.core.content.ContextCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
*
* <p>
* RecyclerView Adapter
* 线 OnItemClickListener OnItemLongClickListener
* </p>
*
* @see TodoFragment
* @see TodoItem
*/
public class TodoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_SECTION = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final List<Object> mItems = new ArrayList<>();
private final Set<Integer> mSelectedPositions = new HashSet<>();
private boolean mChoiceMode;
private OnItemClickListener mItemClickListener;
private OnItemLongClickListener mItemLongClickListener;
public interface OnItemClickListener {
void onItemClick(TodoItem item, int adapterPosition);
void onCheckChanged(TodoItem item, boolean checked);
}
public interface OnItemLongClickListener {
void onItemLongClick(TodoItem item, int adapterPosition);
}
/**
*
*
* @param undone
* @param done
*/
public void setData(List<TodoItem> undone, List<TodoItem> done) {
mItems.clear();
if (!undone.isEmpty()) {
mItems.add("undone");
mItems.addAll(undone);
}
if (!done.isEmpty()) {
mItems.add("done");
mItems.addAll(done);
}
notifyDataSetChanged();
}
/** 设置 item 点击与勾选变更监听器 */
public void setOnItemClickListener(OnItemClickListener l) { mItemClickListener = l; }
/** 设置 item 长按监听器 */
public void setOnItemLongClickListener(OnItemLongClickListener l) { mItemLongClickListener = l; }
/**
*
*
* @param mode true
*/
public void setChoiceMode(boolean mode) {
mChoiceMode = mode;
mSelectedPositions.clear();
notifyDataSetChanged();
}
/** 判断是否处于多选模式 */
public boolean isChoiceMode() { return mChoiceMode; }
/**
*
*
* @param position
* @param checked true
*/
public void setChecked(int position, boolean checked) {
if (checked) mSelectedPositions.add(position);
else mSelectedPositions.remove(position);
notifyItemChanged(position);
}
/** 判断指定位置是否被选中 */
public boolean isChecked(int position) { return mSelectedPositions.contains(position); }
/** 获取当前选中的待办数量 */
public int getSelectedCount() { return mSelectedPositions.size(); }
/**
* ID
*/
public List<Long> getSelectedIds() {
List<Long> ids = new ArrayList<>();
for (Integer pos : mSelectedPositions) {
Object o = mItems.get(pos);
if (o instanceof TodoItem) ids.add(((TodoItem) o).getId());
}
return ids;
}
/**
* TodoItem
*
* @param select true false
*/
public void selectAll(boolean select) {
mSelectedPositions.clear();
if (select) {
for (int i = 0; i < mItems.size(); i++) {
if (mItems.get(i) instanceof TodoItem) mSelectedPositions.add(i);
}
}
notifyDataSetChanged();
}
/**
*
*/
public boolean isAllSelected() {
int itemCount = 0;
for (Object o : mItems) if (o instanceof TodoItem) itemCount++;
return itemCount > 0 && mSelectedPositions.size() == itemCount;
}
private boolean isSection(int position) {
return mItems.get(position) instanceof String;
}
private TodoItem getItemAt(int position) {
Object o = mItems.get(position);
return o instanceof TodoItem ? (TodoItem) o : null;
}
@Override
public int getItemViewType(int position) {
return isSection(position) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_SECTION) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_todo_section, parent, false);
return new SectionHolder(v);
} else {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_todo, parent, false);
return new ItemHolder(v);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof SectionHolder) {
String section = (String) mItems.get(position);
((SectionHolder) holder).bind(section.equals("undone")
? holder.itemView.getContext().getString(R.string.todo_section_undone)
: holder.itemView.getContext().getString(R.string.todo_section_done));
} else if (holder instanceof ItemHolder) {
TodoItem item = getItemAt(position);
if (item != null) {
((ItemHolder) holder).bind(item, mChoiceMode, mSelectedPositions.contains(position));
}
}
}
@Override
public int getItemCount() { return mItems.size(); }
private class SectionHolder extends RecyclerView.ViewHolder {
TextView tvTitle;
SectionHolder(View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_section_title);
}
void bind(String title) {
tvTitle.setText(title);
}
}
private class ItemHolder extends RecyclerView.ViewHolder {
CheckBox cbTodo;
TextView tvContent;
ItemHolder(View itemView) {
super(itemView);
cbTodo = itemView.findViewById(R.id.cb_todo);
tvContent = itemView.findViewById(R.id.tv_todo_content);
}
void bind(TodoItem item, boolean choiceMode, boolean checked) {
// 先移除监听器,避免 setChecked 触发复用前的旧回调导致错更新或闪退
cbTodo.setOnCheckedChangeListener(null);
cbTodo.setChecked(item.isDone());
tvContent.setText(item.getContent());
if (item.isDone()) {
tvContent.setPaintFlags(tvContent.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
tvContent.setTextColor(ContextCompat.getColor(itemView.getContext(), android.R.color.darker_gray));
} else {
tvContent.setPaintFlags(tvContent.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
tvContent.setTextColor(ContextCompat.getColor(itemView.getContext(), R.color.text_primary));
}
cbTodo.setVisibility(View.VISIBLE);
if (choiceMode) {
cbTodo.setChecked(checked);
cbTodo.setOnCheckedChangeListener((b, isChecked) -> setChecked(getAdapterPosition(), isChecked));
} else {
cbTodo.setOnCheckedChangeListener((b, isChecked) -> {
if (mItemClickListener != null) mItemClickListener.onCheckChanged(item, isChecked);
});
}
itemView.setOnClickListener(v -> {
if (choiceMode) {
setChecked(getAdapterPosition(), !mSelectedPositions.contains(getAdapterPosition()));
} else if (mItemClickListener != null) {
mItemClickListener.onItemClick(item, getAdapterPosition());
}
});
itemView.setOnLongClickListener(v -> {
if (mItemLongClickListener != null) {
mItemLongClickListener.onItemLongClick(item, getAdapterPosition());
}
return false;
});
}
}
}

@ -1,61 +0,0 @@
package net.micode.notes.todo;
/**
* 广
* <p>
* AlarmManager 广
* Android 8.0+
* </p>
*
* @see TodoReminderManager
*/
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.Build;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
import net.micode.notes.ui.MainActivity;
public class TodoAlarmReceiver extends BroadcastReceiver {
private static final String CHANNEL_ID = "todo_reminder";
@Override
public void onReceive(Context context, Intent intent) {
long todoId = intent.getLongExtra("todo_id", 0);
String content = intent.getStringExtra("todo_content");
if (content == null) content = "待办提醒";
createNotificationChannel(context);
Intent openIntent = new Intent(context, MainActivity.class);
openIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(context, (int) todoId, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle("待办提醒")
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pi)
.setAutoCancel(true);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.notify((int) todoId, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"待办提醒", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.createNotificationChannel(channel);
}
}
}

@ -1,97 +0,0 @@
package net.micode.notes.todo;
/**
* 广
* <p>
* Geofencing 广
* 使 Google Play Services Geofencing API
* </p>
*
* @see TodoReminderManager
* @see com.google.android.gms.location.GeofencingClient
*/
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.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent;
import net.micode.notes.R;
import net.micode.notes.ui.MainActivity;
import java.util.List;
public class TodoGeofenceReceiver extends BroadcastReceiver {
private static final String TAG = "TodoGeofenceReceiver";
private static final String CHANNEL_ID = "todo_geofence";
@Override
public void onReceive(Context context, Intent intent) {
GeofencingEvent event = GeofencingEvent.fromIntent(intent);
if (event == null) return;
if (event.hasError()) {
Log.e(TAG, "Geofence error: " + event.getErrorCode());
return;
}
int transition = event.getGeofenceTransition();
if (transition != Geofence.GEOFENCE_TRANSITION_ENTER) return;
List<Geofence> triggering = event.getTriggeringGeofences();
if (triggering == null || triggering.isEmpty()) return;
for (Geofence g : triggering) {
String requestId = g.getRequestId();
if (requestId.startsWith("todo_")) {
long todoId = Long.parseLong(requestId.substring(5));
String content = intent.getStringExtra("todo_content");
String locationName = intent.getStringExtra("location_name");
if (content == null) {
net.micode.notes.data.TodoDao dao = new net.micode.notes.data.TodoDao(context);
net.micode.notes.todo.TodoItem item = dao.getById(todoId);
content = item != null ? item.getContent() : "待办";
if (item != null && locationName == null) locationName = item.getLocationName();
}
String title = "到达" + (locationName != null ? locationName : "目标地点");
showNotification(context, (int) todoId, title, content);
}
}
}
private void showNotification(Context context, int id, String title, String content) {
createNotificationChannel(context);
Intent openIntent = new Intent(context, MainActivity.class);
openIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pi = PendingIntent.getActivity(context, id, openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pi)
.setAutoCancel(true);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.notify(id, builder.build());
}
private void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
"待办地点提醒", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) nm.createNotificationChannel(channel);
}
}
}

@ -1,62 +0,0 @@
package net.micode.notes.todo;
/**
*
*/
public class TodoItem {
/** 提醒类型:无 */
public static final int REMINDER_NONE = 0;
/** 提醒类型:时间 */
public static final int REMINDER_TIME = 1;
/** 提醒类型:地点 */
public static final int REMINDER_LOCATION = 2;
private long id;
private String content;
private boolean isDone;
private int reminderType;
private long reminderTimestamp;
private double latitude;
private double longitude;
private String locationName;
private long createdTime;
public TodoItem() {
this.id = 0;
this.content = "";
this.isDone = false;
this.reminderType = REMINDER_NONE;
this.reminderTimestamp = 0;
this.latitude = 0;
this.longitude = 0;
this.locationName = "";
this.createdTime = System.currentTimeMillis();
}
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content != null ? content : ""; }
public boolean isDone() { return isDone; }
public void setDone(boolean done) { isDone = done; }
public int getReminderType() { return reminderType; }
public void setReminderType(int reminderType) { this.reminderType = reminderType; }
public long getReminderTimestamp() { return reminderTimestamp; }
public void setReminderTimestamp(long reminderTimestamp) { this.reminderTimestamp = reminderTimestamp; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public String getLocationName() { return locationName; }
public void setLocationName(String locationName) { this.locationName = locationName != null ? locationName : ""; }
public long getCreatedTime() { return createdTime; }
public void setCreatedTime(long createdTime) { this.createdTime = createdTime; }
}

@ -1,121 +0,0 @@
package net.micode.notes.todo;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingClient;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationServices;
import net.micode.notes.data.TodoDao;
import java.util.ArrayList;
import java.util.List;
/**
* (AlarmManager) + (Geofencing)
*/
public class TodoReminderManager {
private static final String TAG = "TodoReminderManager";
private static final float GEOFENCE_RADIUS_METERS = 100f;
public static void scheduleReminders(Context context, TodoItem item) {
if (item == null || item.getId() <= 0) return;
cancelReminders(context, item.getId());
if (item.getReminderType() == TodoItem.REMINDER_TIME && item.getReminderTimestamp() > 0) {
scheduleTimeReminder(context, item);
} else if (item.getReminderType() == TodoItem.REMINDER_LOCATION
&& item.getLatitude() != 0 && item.getLongitude() != 0) {
scheduleGeofenceReminder(context, item);
}
}
public static void cancelReminders(Context context, long todoId) {
cancelTimeReminder(context, todoId);
cancelGeofenceReminder(context, todoId);
}
private static void scheduleTimeReminder(Context context, TodoItem item) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (am == null) return;
if (item.getReminderTimestamp() <= System.currentTimeMillis()) return;
Intent intent = new Intent(context, TodoAlarmReceiver.class);
intent.putExtra("todo_id", item.getId());
intent.putExtra("todo_content", item.getContent());
PendingIntent pi = PendingIntent.getBroadcast(context, (int) item.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (am.canScheduleExactAlarms()) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
} else {
am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
} else {
am.set(AlarmManager.RTC_WAKEUP, item.getReminderTimestamp(), pi);
}
Log.d(TAG, "Scheduled time reminder for todo " + item.getId());
}
private static void cancelTimeReminder(Context context, long todoId) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (am == null) return;
Intent intent = new Intent(context, TodoAlarmReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(context, (int) todoId, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
am.cancel(pi);
}
private static void scheduleGeofenceReminder(Context context, TodoItem item) {
GeofencingClient client = LocationServices.getGeofencingClient(context);
Geofence geofence = new Geofence.Builder()
.setRequestId("todo_" + item.getId())
.setCircularRegion(item.getLatitude(), item.getLongitude(), GEOFENCE_RADIUS_METERS)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
.build();
List<Geofence> list = new ArrayList<>();
list.add(geofence);
GeofencingRequest request = new GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
.addGeofences(list)
.build();
Intent intent = new Intent(context, TodoGeofenceReceiver.class);
intent.putExtra("todo_id", item.getId());
intent.putExtra("todo_content", item.getContent());
intent.putExtra("location_name", item.getLocationName());
PendingIntent pi = PendingIntent.getBroadcast(context, (int) item.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
client.addGeofences(request, pi)
.addOnSuccessListener(aVoid -> Log.d(TAG, "Geofence added for todo " + item.getId()))
.addOnFailureListener(e -> Log.e(TAG, "Geofence add failed: " + e.getMessage()));
}
private static void cancelGeofenceReminder(Context context, long todoId) {
GeofencingClient client = LocationServices.getGeofencingClient(context);
List<String> ids = new ArrayList<>();
ids.add("todo_" + todoId);
client.removeGeofences(ids)
.addOnSuccessListener(aVoid -> Log.d(TAG, "Geofence removed for todo " + todoId))
.addOnFailureListener(e -> Log.e(TAG, "Geofence remove failed: " + e.getMessage()));
}
/** 在删除待办时调用,取消所有提醒 */
public static void cancelAllForTodo(Context context, long todoId) {
cancelReminders(context, todoId);
}
}

@ -1,91 +0,0 @@
package net.micode.notes.tool;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import net.micode.notes.R;
/**
*
* <p>
*
* MediaProjection 便
* Android 10+ FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
* </p>
*
* @see FloatingViewManager
* @see ScreenCaptureManager
*/
public class FloatingService extends Service {
private FloatingViewManager mManager;
private ScreenCaptureManager mCaptureManager;
@Override
public IBinder onBind(Intent intent) { return null; }
@Override
public void onCreate() {
super.onCreate();
mManager = new FloatingViewManager(this);
startForegroundServiceNotification();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && intent.hasExtra("data")) {
int resultCode = intent.getIntExtra("resultCode", -1);
Intent data = intent.getParcelableExtra("data");
MediaProjectionManager mm = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
MediaProjection mp = mm.getMediaProjection(resultCode, data);
// 关键:确保这里每次都是 new 出来的
mCaptureManager = new ScreenCaptureManager(this);
mCaptureManager.setMediaProjection(mp);
mManager.setCaptureManager(mCaptureManager);
}
mManager.showFloatingBall();
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
mManager.removeFloatingBall();
}
private void startForegroundServiceNotification() {
String channelId = "inspiration_channel";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId, "灵感球服务", NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
}
// 1. 构建通知,删掉了那行报错的代码
Notification notification = new NotificationCompat.Builder(this, channelId)
.setContentTitle("灵感球已开启")
.setContentText("点击悬浮球,捕捉瞬间灵感")
.setSmallIcon(R.drawable.icon_app)
.build();
// 2. 在这里指定前台服务类型(这是最标准的写法)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 (Q) 以上支持指定类型
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
} else {
// 老版本直接启动
startForeground(1, notification);
}
}
}

@ -1,302 +0,0 @@
package net.micode.notes.tool;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import net.micode.notes.tool.DataUtils;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.util.Log;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
// 关键:必须导入内部类 NoteColumns 和 DataColumns
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.DataColumns;
import java.io.File;
import java.io.FileOutputStream;
/**
* (Floating Ball Manager)
* <p>
*
* 1. UI
* 2. +
* 3. {@link ScreenCaptureManager} {@link MediaUtils}
*/
public class FloatingViewManager {
private Context mContext;
private WindowManager mWindowManager;
private View mFloatingView;
private WindowManager.LayoutParams mParams;
private ScreenCaptureManager mCaptureManager;
private int mScreenWidth, mScreenHeight;
/**
*
*
* @param context WindowManager DisplayMetrics
*/
public FloatingViewManager(Context context) {
mContext = context;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics m = context.getResources().getDisplayMetrics();
mScreenWidth = m.widthPixels;
mScreenHeight = m.heightPixels;
}
/**
* capture
*
* @param scm null
*/
public void setCaptureManager(ScreenCaptureManager scm) {
this.mCaptureManager = scm;
}
/**
*
* 使 TYPE_APPLICATION_OVERLAY TYPE_PHONE
*/
public void showFloatingBall() {
if (mFloatingView != null) return;
mParams = new WindowManager.LayoutParams();
mParams.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;
mParams.format = PixelFormat.RGBA_8888;
mParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
mParams.gravity = Gravity.LEFT | Gravity.TOP;
mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
mParams.x = mScreenWidth;
mParams.y = mScreenHeight / 2;
mFloatingView = LayoutInflater.from(mContext).inflate(R.layout.layout_floating_ball, null);
initTouchListener();
mWindowManager.addView(mFloatingView, mParams);
}
/**
*
*/
private void initTouchListener() {
View ball = mFloatingView.findViewById(R.id.iv_floating_ball);
ball.setOnTouchListener(new View.OnTouchListener() {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
private boolean isDrag;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = mParams.x; initialY = mParams.y;
initialTouchX = event.getRawX(); initialTouchY = event.getRawY();
isDrag = false;
return true;
case MotionEvent.ACTION_MOVE:
int dx = (int) (event.getRawX() - initialTouchX);
int dy = (int) (event.getRawY() - initialTouchY);
if (!isDrag && (Math.abs(dx) > 20 || Math.abs(dy) > 20)) isDrag = true;
if (isDrag) {
mParams.x = initialX + dx; mParams.y = initialY + dy;
mWindowManager.updateViewLayout(mFloatingView, mParams);
}
return true;
case MotionEvent.ACTION_UP:
if (!isDrag) onBallClick(); else snapToEdge();
return true;
}
return false;
}
});
}
/**
*
*/
private void snapToEdge() {
mParams.x = (mParams.x + mFloatingView.getWidth() / 2 < mScreenWidth / 2) ? 0 : mScreenWidth;
mWindowManager.updateViewLayout(mFloatingView, mParams);
}
/**
*
* 3
*/
private void onBallClick() {
if (mCaptureManager == null) {
Toast.makeText(mContext, "截屏服务未初始化", Toast.LENGTH_SHORT).show();
return;
}
// 1. 视觉与触觉反馈
mFloatingView.animate().scaleX(0.7f).scaleY(0.7f).setDuration(100).withEndAction(() -> {
mFloatingView.animate().scaleX(1.0f).scaleY(1.0f).setDuration(100).start();
}).start();
Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE));
// 2. 隐藏小球开始截图
mFloatingView.setVisibility(View.GONE);
// 增加一个“超时保险”:如果 3 秒还没截到图,强行把球变回来
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
final Runnable timeoutRunnable = () -> {
if (mFloatingView.getVisibility() == View.GONE) {
mFloatingView.setVisibility(View.VISIBLE);
Toast.makeText(mContext, "截图响应超时", Toast.LENGTH_SHORT).show();
}
};
timeoutHandler.postDelayed(timeoutRunnable, 3000);
// 3. 执行截图
new Handler(Looper.getMainLooper()).postDelayed(() -> {
mCaptureManager.capture(bitmap -> {
// 截图有回应了,移除超时保险
timeoutHandler.removeCallbacks(timeoutRunnable);
mFloatingView.setVisibility(View.VISIBLE);
if (bitmap != null) {
handleCapturedBitmap(bitmap);
} else {
Toast.makeText(mContext, "截图失败,请重试", Toast.LENGTH_SHORT).show();
}
});
}, 200); // 稍微延长一点等待时间,确保小球完全消失
}
/**
* Bitmap JPEG
*
* @param bitmap null
*/
private void handleCapturedBitmap(Bitmap bitmap) {
try {
File file = MediaUtils.createImageFile(mContext);
try (FileOutputStream out = new FileOutputStream(file)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, out);
}
// 使用 getCanonicalPath() 规范化路径,符合 G.FIO.01
String path = file.getCanonicalPath();
showInputWindow(path);
} catch (Exception e) { e.printStackTrace(); }
}
/**
* saveInspirationToDb
*
* @param imagePath
*/
private void showInputWindow(String imagePath) {
WindowManager.LayoutParams p = new WindowManager.LayoutParams();
p.type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE;
p.format = PixelFormat.RGBA_8888;
p.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND;
p.dimAmount = 0.5f;
p.width = WindowManager.LayoutParams.MATCH_PARENT;
p.height = WindowManager.LayoutParams.WRAP_CONTENT;
p.gravity = Gravity.CENTER;
View inputView = LayoutInflater.from(mContext).inflate(R.layout.layout_floating_input, null);
EditText et = inputView.findViewById(R.id.et_inspiration);
inputView.findViewById(R.id.btn_send_inspiration).setOnClickListener(v -> {
String text = et.getText().toString();
saveInspirationToDb(imagePath, text);
mWindowManager.removeView(inputView);
});
mWindowManager.addView(inputView, p);
et.requestFocus();
// 延时弹出软键盘
new Handler(Looper.getMainLooper()).postDelayed(() -> {
InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(et, InputMethodManager.SHOW_IMPLICIT);
}, 200);
}
/**
* + 便
* 线 Note Data snippet
*
* @param path
* @param text
*/
private void saveInspirationToDb(String path, String text) {
new Thread(() -> {
try {
// 1. 插入 Note 主表snippet 先占位,插入 Data 后会被触发器覆盖,再在下面按规则写回)
ContentValues v = new ContentValues();
long now = System.currentTimeMillis();
v.put(net.micode.notes.data.Notes.NoteColumns.PARENT_ID, net.micode.notes.data.Notes.ID_ROOT_FOLDER);
v.put(net.micode.notes.data.Notes.NoteColumns.SNIPPET, "");
v.put(net.micode.notes.data.Notes.NoteColumns.TYPE, 0); // TYPE_NOTE
v.put(net.micode.notes.data.Notes.NoteColumns.MODIFIED_DATE, now);
v.put(net.micode.notes.data.Notes.NoteColumns.CREATED_DATE, now);
v.put(net.micode.notes.data.Notes.NoteColumns.LOCAL_MODIFIED, 1);
v.put(net.micode.notes.data.Notes.NoteColumns.BG_COLOR_ID, 0);
Uri uri = mContext.getContentResolver().insert(net.micode.notes.data.Notes.CONTENT_NOTE_URI, v);
if (uri == null) return;
long noteId = android.content.ContentUris.parseId(uri);
// 2. 插入 Data 附表
ContentValues dv = new ContentValues();
dv.put(Notes.DataColumns.NOTE_ID, noteId);
dv.put(Notes.DataColumns.MIME_TYPE, "vnd.android.cursor.item/text_note");
String fullContent = "<img src=\"" + path + "\"/>\n" + (text == null ? "" : text);
dv.put(Notes.DataColumns.CONTENT, fullContent);
dv.put(Notes.DataColumns.DATA1, 0);
mContext.getContentResolver().insert(Notes.CONTENT_DATA_URI, dv);
// 3. 灵感便签标题修复:优先用用户输入的文字作为 snippet仅无文字时用「图片便签」禁止用文件路径
String snippet = DataUtils.computeNoteSnippet(fullContent);
ContentValues uv = new ContentValues();
uv.put(Notes.NoteColumns.SNIPPET, snippet);
mContext.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), uv, null, null);
new Handler(Looper.getMainLooper()).post(() ->
Toast.makeText(mContext, "灵感已存入灵感箱", Toast.LENGTH_SHORT).show());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
/**
* showFloatingBall
*/
public void removeFloatingBall() {
if (mFloatingView != null) {
mWindowManager.removeView(mFloatingView);
mFloatingView = null;
}
}
}

@ -1,249 +0,0 @@
package net.micode.notes.tool;
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
*
* IOUriBitmap
*
*/
public class MediaUtils {
private static final String TAG = "MediaUtils";
private static final String IMAGE_DIR_NAME = "images";
/**
*
* /Android/data//files/Pictures/images/
*/
public static File createImageFile(Context context) throws IOException {
return createImageFileWithExtension(context, ".jpg");
}
/**
* //
*
* @param context getExternalFilesDir
* @param extension ".jpg"".png"
* @return Pictures/images
*/
private static File createImageFileWithExtension(Context context, String extension) throws IOException {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "NOTE_" + timeStamp + "_";
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (storageDir != null && !storageDir.exists()) {
storageDir.mkdirs();
}
if (extension == null || !extension.startsWith(".")) {
extension = ".jpg";
}
return File.createTempFile(imageFileName, extension, storageDir);
}
/**
* Uri MIME PNG jpg/webp
*
* @param context ContentResolver.getType
* @param uri Uri
* @return ".jpg"".png"
*/
private static String getExtensionFromUri(Context context, Uri uri) {
if (uri == null) return ".jpg";
String mime = context.getContentResolver().getType(uri);
if (mime != null) {
if (mime.contains("png")) return ".png";
if (mime.contains("webp")) return ".webp";
if (mime.contains("gif")) return ".gif";
}
return ".jpg";
}
/**
* Uri // ContentProvider file Uri
* 便 Uri
* @return null
*/
public static String copyUriToInternalStorage(Context context, Uri sourceUri) {
if (context == null || sourceUri == null) return null;
// 1. file:// 方案:截图或部分文件管理器返回的是文件路径,直接复制文件
if ("file".equalsIgnoreCase(sourceUri.getScheme())) {
String path = sourceUri.getPath();
if (path != null && !path.isEmpty()) {
return copyFileToInternalStorage(context, new File(path));
}
Log.e(TAG, "file Uri has no path: " + sourceUri);
return null;
}
// 2. content:// 方案:相册、微信等返回的 Content Uri
ContentResolver contentResolver = context.getContentResolver();
String extension = getExtensionFromUri(context, sourceUri);
// 2a. 优先尝试 openInputStream多数相册/MediaStore 支持)
InputStream inputStream = null;
try {
inputStream = contentResolver.openInputStream(sourceUri);
} catch (SecurityException e) {
Log.w(TAG, "openInputStream SecurityException, try openFileDescriptor: " + e.getMessage());
} catch (java.io.FileNotFoundException e) {
Log.w(TAG, "openInputStream FileNotFoundException, try openFileDescriptor: " + e.getMessage());
}
if (inputStream != null) {
String path = copyStreamToInternalStorage(context, inputStream, extension);
closeQuietly(inputStream);
return path;
}
// 2b. 部分应用(如微信、某些截图)仅支持 openFileDescriptor用此方式读取
try {
android.os.ParcelFileDescriptor pfd = contentResolver.openFileDescriptor(sourceUri, "r");
if (pfd != null) {
FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
String path = copyStreamToInternalStorage(context, fis, extension);
closeQuietly(fis);
try { pfd.close(); } catch (IOException ignored) { }
return path;
}
} catch (SecurityException e) {
Log.e(TAG, "openFileDescriptor SecurityException: " + e.getMessage());
} catch (IOException e) {
Log.e(TAG, "openFileDescriptor IOException: " + e.getMessage());
}
Log.e(TAG, "copyUriToInternalStorage failed for: " + sourceUri);
return null;
}
/**
* file:// Uri 或已有路径)
*/
private static String copyFileToInternalStorage(Context context, File sourceFile) {
if (sourceFile == null || !sourceFile.exists() || !sourceFile.canRead()) {
String pathForLog = "null";
if (sourceFile != null) {
try {
pathForLog = sourceFile.getCanonicalPath();
} catch (IOException e) {
pathForLog = "(path unavailable)";
}
}
Log.e(TAG, "Source file not readable: " + pathForLog);
return null;
}
String ext = ".jpg";
String name = sourceFile.getName().toLowerCase();
if (name.endsWith(".png")) ext = ".png";
else if (name.endsWith(".webp")) ext = ".webp";
else if (name.endsWith(".gif")) ext = ".gif";
FileInputStream fis = null;
try {
fis = new FileInputStream(sourceFile);
return copyStreamToInternalStorage(context, fis, ext);
} catch (IOException e) {
Log.e(TAG, "copyFileToInternalStorage failed", e);
return null;
} finally {
closeQuietly(fis);
}
}
/**
*
*/
private static String copyStreamToInternalStorage(Context context, InputStream input, String extension) {
FileOutputStream outputStream = null;
try {
File targetFile = createImageFileWithExtension(context, extension);
outputStream = new FileOutputStream(targetFile);
byte[] buffer = new byte[8192];
int length;
while ((length = input.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.flush();
return targetFile.getCanonicalPath();
} catch (IOException e) {
Log.e(TAG, "Failed to copy stream to internal storage", e);
return null;
} finally {
closeQuietly(outputStream);
}
}
/**
*
*/
private static void closeQuietly(OutputStream os) {
if (os != null) try { os.close(); } catch (IOException ignored) { }
}
/**
*
*/
private static void closeQuietly(InputStream is) {
if (is != null) try { is.close(); } catch (IOException ignored) { }
}
/**
* Bitmap OOM (Out Of Memory)
* @param path
* @param reqWidth
* @param reqHeight
*/
public static Bitmap getCompressedBitmap(String path, int reqWidth, int reqHeight) {
// 1. 只读取图片的尺寸信息,不加载像素到内存
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
// 2. 计算压缩比例
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 3. 真正加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}
/**
* Bitmap (inSampleSize)使 OOM
*
* @param options inJustDecodeBounds=true Options
* @param reqWidth
* @param reqHeight
* @return 2 1248
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 计算最大的 2 的幂次,保证宽高仍大于期望宽高
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}

@ -1,30 +0,0 @@
package net.micode.notes.tool;
/**
*
*
* 退
*/
public class PrivacySpaceManager {
private static volatile boolean sInPrivacySpace = false;
/** 是否为隐私空间模式 */
public static boolean isInPrivacySpace() {
return sInPrivacySpace;
}
/** 进入隐私空间(由 Mi 管家输入「隐私空间」触发) */
public static void enterPrivacySpace() {
sInPrivacySpace = true;
}
/**
* 退
* 退
*
*/
public static void exitPrivacySpace() {
sInPrivacySpace = false;
}
}

@ -1,110 +0,0 @@
package net.micode.notes.tool;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay;
import android.media.Image;
import android.media.ImageReader;
import android.media.projection.MediaProjection;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import java.nio.ByteBuffer;
/**
*
* <p>
* MediaProjection Bitmap
* FloatingViewManager 使便
* ImageReader VirtualDisplay
* </p>
*
* @see FloatingViewManager
* @see FloatingService
*/
public class ScreenCaptureManager {
private MediaProjection mMediaProjection;
private int mScreenWidth, mScreenHeight, mScreenDensity;
private ImageReader mImageReader; // 【关键】全局持有,不销毁
private VirtualDisplay mVirtualDisplay;
public ScreenCaptureManager(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics metrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metrics);
mScreenWidth = metrics.widthPixels;
mScreenHeight = metrics.heightPixels;
mScreenDensity = metrics.densityDpi;
// 【关键】构造时就创建好 ImageReader一直复用
// maxImages 设为 2避免 Buffer 不够
mImageReader = ImageReader.newInstance(mScreenWidth, mScreenHeight, PixelFormat.RGBA_8888, 2);
}
public void setMediaProjection(MediaProjection mp) {
this.mMediaProjection = mp;
if (mMediaProjection != null) {
// Android 14 必须注册回调,否则崩
mMediaProjection.registerCallback(new MediaProjection.Callback() {}, null);
}
}
public void capture(final OnCaptureListener listener) {
if (mMediaProjection == null || mImageReader == null) return;
// 【关键】每次只需创建 VirtualDisplay
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenCapture",
mScreenWidth, mScreenHeight, mScreenDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(),
null, null);
// 设置单次监听
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 一旦收到图片,立刻移除监听,防止重复触发
reader.setOnImageAvailableListener(null, null);
// 停止投影 (释放 VirtualDisplay但不释放 ImageReader)
if (mVirtualDisplay != null) {
mVirtualDisplay.release();
mVirtualDisplay = null;
}
Bitmap bitmap = null;
Image image = null;
try {
image = reader.acquireLatestImage();
if (image != null) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * mScreenWidth;
bitmap = Bitmap.createBitmap(mScreenWidth + rowPadding / pixelStride, mScreenHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, mScreenWidth, mScreenHeight);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (image != null) image.close(); // 必须关闭 Image 释放 Buffer
final Bitmap result = bitmap;
new Handler(Looper.getMainLooper()).post(() -> {
if (listener != null) listener.onCaptured(result);
});
}
}
}, null); // 在当前线程(主线程)回调,因为这里逻辑很快,为了避免线程死锁先这样
}
public interface OnCaptureListener {
void onCaptured(Bitmap bitmap);
}
}

@ -1,40 +0,0 @@
package net.micode.notes.tool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 线
* 线
*/
public class ThreadExecutor {
// 单例模式保证整个APP只有一个线程池避免资源浪费
private static ThreadExecutor instance;
// 核心成员ExecutorService 就是 Java 里的“线程池管理者”
private final ExecutorService mService;
// 构造函数私有化:不准外面随便 new
private ThreadExecutor() {
// newSingleThreadExecutor创建一个单线程的线程池。
// 就像开设了一个“单人窗口”,所有任务按顺序排队执行。
// 为什么用单线程?因为数据库写入最好按顺序来,防止两个线程同时写同一个便签导致冲突。
mService = Executors.newSingleThreadExecutor();
}
// 获取唯一实例的方法
public static synchronized ThreadExecutor getInstance() {
if (instance == null) {
instance = new ThreadExecutor();
}
return instance;
}
/**
*
* @param task Runnable
*/
public void execute(Runnable task) {
mService.execute(task);
}
}

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

@ -1,52 +0,0 @@
package net.micode.notes.ui;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;
/**
* / paint descent/bottom ascent/top
* 0
*/
public class CenterImageSpan extends ImageSpan {
public CenterImageSpan(Drawable d, String source) {
super(d, source);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
if (rect.isEmpty()) {
return 0;
}
int w = rect.right;
int h = rect.bottom;
if (fm != null) {
// 只向上扩展行高以容纳图片,保留原有 descent/bottom避免多行/多图时行高错位
fm.ascent = Math.min(fm.ascent, -h);
fm.top = Math.min(fm.top, -h);
}
return w;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
Drawable b = getDrawable();
canvas.save();
// 调试模式:不计算居中,直接画在 x 坐标(靠左)
// y 是基线位置bottom 是行底
// 我们让图片底部对齐到底部
int transY = bottom - b.getBounds().bottom;
// 暂时直接用 x确保能看见
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
}

@ -1,126 +0,0 @@
package net.micode.notes.ui;
/**
* Mi
* <p>
* RecyclerView Adapter AI TYPE_USER TYPE_AI
* AI
* </p>
*
* @see net.micode.notes.model.ChatMessage
* @see net.micode.notes.ui.MiStewardFragment
*/
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.model.ChatMessage;
import java.util.ArrayList;
import java.util.List;
public class ChatAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<ChatMessage> mData = new ArrayList<>();
public void addMessage(ChatMessage message) {
mData.add(message);
notifyItemInserted(mData.size() - 1);
}
public void clear() {
mData.clear();
notifyDataSetChanged();
}
// 获取最后一条消息(用于流式更新)
public ChatMessage getLastMessage() {
if (mData.isEmpty()) return null;
return mData.get(mData.size() - 1);
}
public void updateLastMessage() {
if (!mData.isEmpty()) {
notifyItemChanged(mData.size() - 1);
}
}
@Override
public int getItemViewType(int position) {
return mData.get(position).getType();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == ChatMessage.TYPE_USER) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_user, parent, false);
return new UserViewHolder(v);
} else {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_ai, parent, false);
return new AIViewHolder(v);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
ChatMessage msg = mData.get(position);
if (holder instanceof UserViewHolder) {
((UserViewHolder) holder).tvContent.setText(msg.getContent());
} else if (holder instanceof AIViewHolder) {
AIViewHolder aiHolder = (AIViewHolder) holder;
if (msg.isThinking()) {
aiHolder.layoutThinking.setVisibility(View.VISIBLE);
aiHolder.tvContent.setVisibility(View.GONE);
aiHolder.layoutRefs.setVisibility(View.GONE);
} else {
aiHolder.layoutThinking.setVisibility(View.GONE);
aiHolder.tvContent.setVisibility(View.VISIBLE);
aiHolder.tvContent.setText(msg.getContent());
// 处理参考资料
if (msg.getReferences() != null && !msg.getReferences().isEmpty()) {
aiHolder.layoutRefs.setVisibility(View.VISIBLE);
StringBuilder refs = new StringBuilder();
for (String ref : msg.getReferences()) {
refs.append("📄 ").append(ref).append("\n");
}
aiHolder.tvRefs.setText(refs.toString().trim());
} else {
aiHolder.layoutRefs.setVisibility(View.GONE);
}
}
}
}
@Override
public int getItemCount() {
return mData.size();
}
static class UserViewHolder extends RecyclerView.ViewHolder {
TextView tvContent;
UserViewHolder(View itemView) {
super(itemView);
tvContent = itemView.findViewById(R.id.tv_user_content);
}
}
static class AIViewHolder extends RecyclerView.ViewHolder {
View layoutThinking;
TextView tvContent;
View layoutRefs;
TextView tvRefs;
AIViewHolder(View itemView) {
super(itemView);
layoutThinking = itemView.findViewById(R.id.layout_thinking);
tvContent = itemView.findViewById(R.id.tv_ai_content);
layoutRefs = itemView.findViewById(R.id.layout_references);
tvRefs = itemView.findViewById(R.id.tv_reference_list);
}
}
}

@ -1,142 +0,0 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.Switch;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.FloatingService;
public class InspirationFragment extends Fragment {
private ListView mListView;
private NotesListAdapter mAdapter;
private BackgroundQueryHandler mQueryHandler;
private Switch mSwitchFloating;
private static final int QUERY_TOKEN = 202;
private static final int REQUEST_CODE_OVERLAY_PERMISSION = 1001;
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1002;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inspiration, container, false);
initViews(view);
return view;
}
private void initViews(View view) {
mListView = view.findViewById(R.id.inspiration_list);
mSwitchFloating = view.findViewById(R.id.switch_floating_ball);
mAdapter = new NotesListAdapter(getActivity());
mListView.setAdapter(mAdapter);
mQueryHandler = new BackgroundQueryHandler(getActivity().getContentResolver());
mListView.setOnItemClickListener(new android.widget.AdapterView.OnItemClickListener() {
@Override
public void onItemClick(android.widget.AdapterView<?> parent, View view, int position, long id) {
// 跳转到编辑页 (复用 NoteEditActivity)
Intent intent = new Intent(getActivity(), NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, id); // 传入便签 ID
startActivity(intent);
}
});
mSwitchFloating.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
checkAndStartService();
} else {
stopFloatingService();
}
}
});
}
private void checkAndStartService() {
// 1. 检查悬浮窗权限
if (!Settings.canDrawOverlays(getContext())) {
Toast.makeText(getActivity(), "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION);
mSwitchFloating.setChecked(false);
return;
}
// 2. 申请截屏权限
MediaProjectionManager mm = (MediaProjectionManager) getActivity().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(mm.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(getContext())) {
checkAndStartService();
}
} else if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
if (resultCode == Activity.RESULT_OK && data != null) {
startFloatingService(resultCode, data);
} else {
mSwitchFloating.setChecked(false);
Toast.makeText(getActivity(), "未授权截屏,无法开启", Toast.LENGTH_SHORT).show();
}
}
}
private void startFloatingService(int resultCode, Intent data) {
Intent intent = new Intent(getActivity(), FloatingService.class);
intent.putExtra("resultCode", resultCode);
intent.putExtra("data", data);
getActivity().startForegroundService(intent);
}
private void stopFloatingService() {
Intent intent = new Intent(getActivity(), FloatingService.class);
getActivity().stopService(intent);
}
@Override
public void onStart() {
super.onStart();
refreshList();
}
private void refreshList() {
String selection = NoteColumns.PARENT_ID + "=?";
String[] selectionArgs = { String.valueOf(Notes.ID_INSPIRATION_FOLDER) };
mQueryHandler.startQuery(QUERY_TOKEN, null, Notes.CONTENT_NOTE_URI,
NoteItemData.PROJECTION, selection, selectionArgs, NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
public BackgroundQueryHandler(ContentResolver cr) { super(cr); }
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (token == QUERY_TOKEN) mAdapter.changeCursor(cursor);
}
}
}

@ -1,178 +0,0 @@
package net.micode.notes.ui;
/**
* Activity
* <p>
* Mi / 便 / ViewPager2 Drawer
*
* </p>
*
* @see MiStewardFragment
* @see NotesListFragment
* @see TodoFragment
*/
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.navigation.NavigationView;
import net.micode.notes.R;
public class MainActivity extends AppCompatActivity {
private ViewPager2 mViewPager;
private BottomNavigationView mBottomNav;
private DrawerLayout mDrawerLayout;
private NavigationView mNavView;
/** 便签列表页引用,由 NotesListFragment 注册,用于侧边栏菜单回调 */
private NotesListFragment mNotesListFragment;
private View mMainContentRoot;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewPager = findViewById(R.id.view_pager);
mBottomNav = findViewById(R.id.bottom_navigation);
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavView = findViewById(R.id.nav_view);
mMainContentRoot = findViewById(R.id.main_content_root);
mViewPager.setAdapter(new FragmentStateAdapter(this) {
@NonNull
@Override
public Fragment createFragment(int position) {
if (position == 0) {
return new MiStewardFragment();
} else if (position == 1) {
return new NotesListFragment();
} else {
return new TodoFragment();
}
}
@Override
public int getItemCount() {
return 3;
}
});
mBottomNav.setOnItemSelectedListener(item -> {
if (item.getItemId() == R.id.nav_mi_steward) {
mViewPager.setCurrentItem(0);
return true;
} else if (item.getItemId() == R.id.nav_notes) {
mViewPager.setCurrentItem(1);
return true;
} else if (item.getItemId() == R.id.nav_todo) {
mViewPager.setCurrentItem(2);
return true;
}
return false;
});
mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
if (position == 0) {
mBottomNav.setSelectedItemId(R.id.nav_mi_steward);
} else if (position == 1) {
mBottomNav.setSelectedItemId(R.id.nav_notes);
} else {
mBottomNav.setSelectedItemId(R.id.nav_todo);
}
}
});
// 侧边栏菜单点击
mNavView.setNavigationItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.drawer_new_note) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.createNewNoteFromDrawer();
}
});
mDrawerLayout.closeDrawers();
return true;
}
if (id == R.id.drawer_new_folder) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.showCreateOrModifyFolderDialogFromDrawer(true);
}
});
mDrawerLayout.closeDrawers();
return true;
}
if (id == R.id.drawer_export) {
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.exportNoteToTextFromDrawer();
}
});
mDrawerLayout.closeDrawers();
return true;
}
return false;
});
}
/** 供便签页 Toolbar 菜单按钮调用,打开侧边栏 */
public void openDrawer() {
if (mDrawerLayout != null) {
mDrawerLayout.openDrawer(mNavView);
}
}
/** 由 NotesListFragment 在 onViewCreated 时注册onDestroyView 时置空 */
public void setNotesListFragment(NotesListFragment fragment) {
mNotesListFragment = fragment;
}
/**
* + 便
* MiStewardFragment
*/
public void enterPrivacySpace() {
int darkColor = ContextCompat.getColor(this, R.color.privacy_space_background);
if (mMainContentRoot != null) {
mMainContentRoot.setBackgroundColor(darkColor);
}
if (mBottomNav != null) {
mBottomNav.setBackgroundColor(darkColor);
}
mViewPager.setCurrentItem(1);
mViewPager.post(() -> {
if (mNotesListFragment != null) {
mNotesListFragment.onEnterPrivacySpace();
}
});
}
/** 退出隐私空间:恢复正常主题 */
public void exitPrivacySpace() {
if (mMainContentRoot != null) {
mMainContentRoot.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_white));
}
if (mBottomNav != null) {
mBottomNav.setBackgroundColor(ContextCompat.getColor(this, R.color.surface_white));
}
}
}

@ -1,253 +0,0 @@
package net.micode.notes.ui;
/**
* Mi AI Fragment
* <p>
* Mi DeepSeek
* RAG 便 AI
*
* </p>
*
* @see ChatAdapter
* @see AIService
* @see NoteRetriever
* @see PromptBuilder
*/
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.ai.AIService;
import net.micode.notes.ai.NoteRetriever;
import net.micode.notes.ai.PromptBuilder;
import net.micode.notes.model.ChatMessage;
import net.micode.notes.tool.FloatingService;
import net.micode.notes.tool.PrivacySpaceManager;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiStewardFragment extends Fragment {
private RecyclerView mRecyclerView;
private ChatAdapter mAdapter;
private EditText mInputBox;
private View mLogoArea;
private Switch mSwitchFloating;
private CheckBox mCbUseKnowledge;
private static final int REQUEST_CODE_OVERLAY_PERMISSION = 1001;
private static final int REQUEST_CODE_SCREEN_CAPTURE = 1002;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_mi_steward, container, false);
initViews(view);
return view;
}
@Override
public void onResume() {
super.onResume();
if (getView() != null && PrivacySpaceManager.isInPrivacySpace()) {
getView().setBackgroundColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.privacy_space_background));
}
}
private void initViews(View view) {
mRecyclerView = view.findViewById(R.id.recycler_view);
mInputBox = view.findViewById(R.id.et_input);
mLogoArea = view.findViewById(R.id.center_logo_area);
mCbUseKnowledge = view.findViewById(R.id.cb_use_knowledge);
mSwitchFloating = view.findViewById(R.id.switch_steward_floating);
Button btnSend = view.findViewById(R.id.btn_send);
ImageView btnNewChat = view.findViewById(R.id.btn_new_chat);
mAdapter = new ChatAdapter();
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setAdapter(mAdapter);
// 1. 发送按钮逻辑
btnSend.setOnClickListener(v -> {
String text = mInputBox.getText().toString().trim();
if (TextUtils.isEmpty(text)) return;
mLogoArea.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
mAdapter.addMessage(new ChatMessage(text));
mInputBox.setText("");
scrollToBottom();
// 检测「隐私空间」四个字,有且仅有此四字时进入隐私空间
if ("隐私空间".equals(text)) {
enterPrivacySpaceFlow();
return;
}
performAIQuery(text);
});
// 2. 新对话按钮逻辑 (左上角)
btnNewChat.setOnClickListener(v -> {
mAdapter.clear();
mLogoArea.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
Toast.makeText(getContext(), "已开启新对话", Toast.LENGTH_SHORT).show();
});
// 3. 灵感球开关逻辑 (右上角)
mSwitchFloating.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
checkAndStartFloatingService();
} else {
Intent intent = new Intent(getActivity(), FloatingService.class);
getActivity().stopService(intent);
Toast.makeText(getActivity(), "灵感球已关闭", Toast.LENGTH_SHORT).show();
}
});
}
private void checkAndStartFloatingService() {
// 第一步:检查悬浮窗权限
if (!Settings.canDrawOverlays(getContext())) {
Toast.makeText(getActivity(), "请授予悬浮窗权限", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION);
mSwitchFloating.setChecked(false); // 暂时拨回,等待授权
return;
}
// 第二步:申请截屏权限
MediaProjectionManager mm = (MediaProjectionManager) getActivity().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
if (mm != null) {
startActivityForResult(mm.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION) {
if (Settings.canDrawOverlays(getContext())) {
checkAndStartFloatingService(); // 重新走流程,进入下一步申请截屏
}
} else if (requestCode == REQUEST_CODE_SCREEN_CAPTURE) {
if (resultCode == Activity.RESULT_OK && data != null) {
// 第三步:启动服务
Intent intent = new Intent(getActivity(), FloatingService.class);
intent.putExtra("resultCode", resultCode);
intent.putExtra("data", data);
getActivity().startForegroundService(intent);
mSwitchFloating.setChecked(true);
Toast.makeText(getActivity(), "灵感球已开启", Toast.LENGTH_SHORT).show();
} else {
mSwitchFloating.setChecked(false);
Toast.makeText(getActivity(), "未授权截屏,无法开启", Toast.LENGTH_SHORT).show();
}
}
}
private void performAIQuery(String userQuery) {
ChatMessage aiMsg = new ChatMessage();
mAdapter.addMessage(aiMsg);
scrollToBottom();
boolean useRAG = mCbUseKnowledge.isChecked();
new Thread(() -> {
List<NoteRetriever.RetrievedNote> relatedNotes = null;
if (useRAG) {
relatedNotes = NoteRetriever.searchRelevantNotes(getContext(), userQuery);
}
String systemPrompt = PromptBuilder.buildSystemPrompt(relatedNotes, useRAG);
AIService.chatWithKnowledge(systemPrompt, userQuery, new AIService.AIResultCallback() {
@Override
public void onSuccess(String result) {
List<String> refs = new ArrayList<>();
Pattern pattern = Pattern.compile("\\[Ref:(.*?)\\]");
Matcher matcher = pattern.matcher(result);
while (matcher.find()) {
refs.add(matcher.group(1));
}
String cleanContent = matcher.replaceAll("").trim();
new Handler(Looper.getMainLooper()).post(() -> {
ChatMessage lastMsg = mAdapter.getLastMessage();
if (lastMsg != null && lastMsg.getType() == ChatMessage.TYPE_AI) {
lastMsg.setThinking(false);
lastMsg.setContent(cleanContent);
lastMsg.setReferences(refs);
mAdapter.updateLastMessage();
scrollToBottom();
}
});
}
@Override
public void onError(String error) {
new Handler(Looper.getMainLooper()).post(() -> {
ChatMessage lastMsg = mAdapter.getLastMessage();
if (lastMsg != null) {
lastMsg.setThinking(false);
lastMsg.setContent("AI 遇到点小麻烦:" + error);
mAdapter.updateLastMessage();
}
});
}
});
}).start();
}
private void scrollToBottom() {
mRecyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
}
/**
* Mi + + 便
*/
private void enterPrivacySpaceFlow() {
// 1. Mi 管家回复
ChatMessage aiMsg = new ChatMessage();
aiMsg.setThinking(false);
aiMsg.setContent("隐私空间已开启~");
mAdapter.addMessage(aiMsg);
scrollToBottom();
// 2. 进入隐私空间模式
PrivacySpaceManager.enterPrivacySpace();
// 3. 通知 MainActivity变暗 UI 并切换到便签列表
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).enterPrivacySpace();
}
}
}

@ -1,511 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
* ... () ...
*/
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
/**
* 便 (UI Component)
* <p>
*
* 1. EditText
* 2.
* - Enter
* - Delete
* 3. ()
* 4.
*/
public class NoteEditText extends EditText {
private static final String TAG = "NoteEditText";
// 当前编辑框在清单列表中的索引位置
private int mIndex;
// 记录按键按下时的光标位置,用于判断是否在行首执行了删除操作
private int mSelectionStartBeforeDelete;
private static final String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
// 链接类型与资源 ID 的映射表
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
* (NoteEditActivity)
*/
public interface OnTextViewChangeListener {
/**
* Delete ()
* @param index
* @param text ()
*/
void onEditTextDelete(int index, String text);
/**
* Enter (/)
* @param index
* @param text
*/
void onEditTextEnter(int index, String text);
/**
* (/)
*/
void onTextChange(int index, boolean hasText);
}
private OnTextViewChangeListener mOnTextViewChangeListener;
/** 伪 Placeholder当光标定位到该行且内容完全匹配时自动清空该行 */
private String mPlaceholderLine1;
private String mPlaceholderLine2;
/**
* 0
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
// ================= [新增] 图片插入核心逻辑 =================
/**
*
* @param imagePath
*/
public void insertImage(String imagePath) {
try {
// 构造标签
String tag = "<img src=\"" + imagePath + "\"/>";
// 插入文本
int start = getSelectionStart();
getText().insert(start, "\n" + tag + "\n");
// 注意:这里我们不再手动 setSpan而是依赖下面的 setTextWithImages 统一处理
// 这样逻辑更统一,不会出现“刚插进去有图,刷新后没图”的尴尬
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* &lt;img src="路径"/&gt; CenterImageSpan
* 便
*
* @param text img
*/
public void setTextWithImages(String text) {
// 1. 先设置纯文本
setText(text);
// 2. 寻找所有的 <img src="..."> 标签
android.text.Editable editable = getText();
String pattern = "<img src=\"(.*?)\"/>"; // 正则表达式抓取路径
java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher m = p.matcher(text);
while (m.find()) {
// 获取路径
String imagePath = m.group(1);
int start = m.start();
int end = m.end();
try {
// 3. 加载图片 (复用之前的逻辑)
int width = getWidth() - getPaddingLeft() - getPaddingRight();
if (width <= 0) width = 1000;
int height = 1000;
android.graphics.Bitmap bitmap = net.micode.notes.tool.MediaUtils.getCompressedBitmap(imagePath, width, height);
if (bitmap != null) {
android.graphics.drawable.Drawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
int imgWidth = drawable.getIntrinsicWidth();
int imgHeight = drawable.getIntrinsicHeight();
if (imgWidth > width) {
float ratio = (float) width / imgWidth;
imgWidth = width;
imgHeight = (int) (imgHeight * ratio);
}
drawable.setBounds(0, 0, imgWidth, imgHeight);
// 4. 使用我们修复版 CenterImageSpan
CenterImageSpan span = new CenterImageSpan(drawable, imagePath);
editable.setSpan(span, start, end, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 强制重新测量与绘制,避免长文本/多图时高度未更新导致内容不可见
requestLayout();
post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
}
/**
* Enter/Delete
*
* @param index 0
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
*
* @param listener null
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* line1 line2 便
*/
public void setPlaceholderLinesForClear(String line1, String line2) {
mPlaceholderLine1 = line1;
mPlaceholderLine2 = line2;
}
/**
* XML inflate 使
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
* defStyle
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* EditText 36
* AT_MOST() DynamicLayout 35
* Layout AT_MOST UNSPECIFIED
* measuredHeight
*/
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int heightMode = View.MeasureSpec.getMode(heightSpec);
if (heightMode == View.MeasureSpec.AT_MOST) {
int unboundedHeight = View.MeasureSpec.makeMeasureSpec(0x3FFFFFFF, View.MeasureSpec.UNSPECIFIED);
super.onMeasure(widthSpec, unboundedHeight);
} else {
super.onMeasure(widthSpec, heightSpec);
}
Layout layout = getLayout();
if (layout != null) {
int contentHeight = layout.getHeight() + getCompoundPaddingTop() + getCompoundPaddingBottom();
setMeasuredDimension(getMeasuredWidth(), contentHeight);
}
}
/**
*
* <p>
*
*
* EditText
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
if (layout != null) {
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
Selection.setSelection(getText(), off);
// 伪 Placeholder若当前行内容为占位文案则清空该行
post(new Runnable() {
@Override
public void run() {
checkAndClearPlaceholderLine();
}
});
}
break;
}
return super.onTouchEvent(event);
}
/**
* mPlaceholderLine1 mPlaceholderLine2
*
*/
private void checkAndClearPlaceholderLine() {
if (TextUtils.isEmpty(mPlaceholderLine1) && TextUtils.isEmpty(mPlaceholderLine2)) return;
android.text.Editable text = getText();
if (text == null) return;
Layout layout = getLayout();
if (layout == null) return;
int selStart = getSelectionStart();
if (selStart < 0) return;
int line = layout.getLineForOffset(selStart);
int lineStart = layout.getLineStart(line);
int lineEnd = layout.getLineEnd(line);
String lineContent = text.subSequence(lineStart, lineEnd).toString().trim();
if (lineContent.equals(mPlaceholderLine1) || lineContent.equals(mPlaceholderLine2)) {
// 只替换占位文案,保留行尾换行符,避免下一行(如时间)顶上来
int replaceEnd = lineEnd;
if (lineEnd > lineStart && lineEnd <= text.length() && text.charAt(lineEnd - 1) == '\n') {
replaceEnd = lineEnd - 1;
}
text.replace(lineStart, replaceEnd, "");
}
}
/**
*
* onKeyUp
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 拦截 Enter 键,防止原生换行(因为我们要创建新 Item
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 记录按下删除键瞬间的光标位置,用于 onKeyUp 判断是否在行首
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
return super.onKeyDown(keyCode, event);
}
/**
* ()
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
// 逻辑:如果删除前光标在 0 位置,且不是第一行,说明用户想合并到上一行
if (mOnTextViewChangeListener != null) {
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true;
}
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
// 逻辑:将当前光标后的文字剪切下来,传给 Listener 创建新行
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
setText(getText().subSequence(0, selectionStart)); // 保留光标前的内容
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
default:
break;
}
return super.onKeyUp(keyCode, event);
}
/**
*
*
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
* ()
* <p>
*
* URLSpan ()
*
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
if (urls.length == 1) {
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
break;
}
}
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 执行跳转 Intent
urls[0].onClick(NoteEditText.this);
return true;
}
});
}
}
super.onCreateContextMenu(menu);
}
// ================= 富文本工具栏:加粗、字号变大、字号变小(仅运行时效果,不持久化) =================
/**
* (StyleSpan BOLD)
*
*/
public void applyBoldToSelection() {
applySpanToSelection(new StyleSpan(Typeface.BOLD), StyleSpan.class, true);
}
/**
* 1.2 (RelativeSizeSpan)
*/
public void applyFontSizeUpToSelection() {
applyRelativeSizeToSelection(1.2f);
}
/**
* 0.8 (RelativeSizeSpan)
*/
public void applyFontSizeDownToSelection() {
applyRelativeSizeToSelection(0.8f);
}
/**
* RelativeSizeSpan
* span
*
* @param proportion 1.2f 1.2
*/
private void applyRelativeSizeToSelection(float proportion) {
android.text.Editable text = getText();
if (text == null || !(text instanceof Spannable)) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start < 0 || end < 0 || start == end) return;
if (start > end) { int t = start; start = end; end = t; }
Spannable sp = (Spannable) text;
RelativeSizeSpan[] existing = sp.getSpans(start, end, RelativeSizeSpan.class);
// 若选区已有相同比例的 RelativeSizeSpan 则移除,否则添加
boolean hasSame = false;
for (RelativeSizeSpan s : existing) {
if (Math.abs(s.getSizeChange() - proportion) < 0.01f) {
sp.removeSpan(s);
hasSame = true;
}
}
if (!hasSame) {
sp.setSpan(new RelativeSizeSpan(proportion), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**
* Span
* @param addSpan span
* @param spanClass / span
* @param toggle true span
*/
private void applySpanToSelection(Object addSpan, Class<?> spanClass, boolean toggle) {
android.text.Editable text = getText();
if (text == null || !(text instanceof Spannable)) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start < 0 || end < 0 || start == end) return;
if (start > end) { int t = start; start = end; end = t; }
Spannable sp = (Spannable) text;
Object[] existing = sp.getSpans(start, end, spanClass);
boolean allCovered = true;
for (Object s : existing) {
int sStart = sp.getSpanStart(s);
int sEnd = sp.getSpanEnd(s);
if (sStart > start || sEnd < end) {
allCovered = false;
break;
}
}
if (existing.length == 0) allCovered = false;
if (toggle && allCovered && existing.length > 0) {
for (Object s : existing) sp.removeSpan(s);
} else {
sp.setSpan(addSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}

@ -1,80 +0,0 @@
package net.micode.notes.ui;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import net.micode.notes.R;
import net.micode.notes.tool.DataUtils;
/**
* 便
* Android 13+
*/
public final class NoteReminderNotifier {
private static final String TAG = "NoteReminderNotifier";
public static final String CHANNEL_ID = "note_reminder_channel";
private static final int SNIPPET_MAX_LEN = 40;
public static void show(Context context, long noteId, Uri noteUri) {
if (context == null || noteUri == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "POST_NOTIFICATIONS not granted");
return;
}
}
String snippet;
try {
snippet = DataUtils.getSnippetById(context.getContentResolver(), noteId);
if (snippet == null) snippet = "";
snippet = snippet.trim();
if (snippet.length() > SNIPPET_MAX_LEN) {
snippet = snippet.substring(0, SNIPPET_MAX_LEN) + "…";
}
} catch (Exception e) {
snippet = context.getString(R.string.note_reminder_default_text);
}
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
context.getString(R.string.note_reminder_channel_name),
NotificationManager.IMPORTANCE_HIGH);
channel.setDescription(context.getString(R.string.note_reminder_channel_desc));
nm.createNotificationChannel(channel);
}
Intent openIntent = new Intent(context, AlarmAlertActivity.class);
openIntent.setData(noteUri);
openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pending = PendingIntent.getActivity(
context,
(int) (noteId % Integer.MAX_VALUE),
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app)
.setContentTitle(context.getString(R.string.note_reminder_title))
.setContentText(snippet.isEmpty() ? context.getString(R.string.note_reminder_default_text) : snippet)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_REMINDER)
.setAutoCancel(true)
.setContentIntent(pending);
try {
NotificationManagerCompat.from(context).notify((int) (noteId % Integer.MAX_VALUE), builder.build());
} catch (SecurityException e) {
Log.e(TAG, "Cannot show notification: " + e.getMessage());
}
}
}

@ -1,40 +0,0 @@
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
/**
* 便广
* 使 Receiver
*/
public class NoteReminderReceiver extends BroadcastReceiver {
private static final String TAG = "NoteReminderReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Uri data = intent.getData();
if (data == null || data.getPathSegments() == null || data.getPathSegments().size() < 2) {
Log.e(TAG, "Alarm intent has no note data");
return;
}
long noteId;
try {
noteId = Long.parseLong(data.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Invalid note id in alarm intent", e);
return;
}
NoteReminderNotifier.show(context, noteId, data);
Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
alertIntent.setData(data);
alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
try {
context.startActivity(alertIntent);
} catch (Exception e) {
Log.d(TAG, "Could not start AlarmAlertActivity: " + e.getMessage());
}
}
}

@ -1,168 +0,0 @@
package net.micode.notes.ui;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
/**
*
* + Logo + + 线
* 退 ANR使 post GlobalLayoutListener
*/
public class SplashActivity extends AppCompatActivity {
private static final int PHASE_ENTER_MS = 900;
private static final int HOLD_MS = 1100;
private static final int PHASE_EXIT_MS = 450;
/** 最大等待时间,超时则直接进入主界面,防止卡死 */
private static final int SAFETY_TIMEOUT_MS = 4000;
private View mContentRoot;
private ImageView mLogo;
private TextView mTitle;
private View mAccent;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mEnterAnimationStarted;
private boolean mGoMainCalled;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
mContentRoot = findViewById(R.id.splash_content);
mLogo = findViewById(R.id.splash_logo);
mTitle = findViewById(R.id.splash_title);
mAccent = findViewById(R.id.splash_accent);
if (mContentRoot == null || mLogo == null || mTitle == null || mAccent == null) {
goToMain();
return;
}
if (mLogo != null) {
mLogo.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
}
// 使用 post 在下一帧启动动画,避免与 GlobalLayoutListener 的 API 差异导致不触发
mContentRoot.post(this::startEnterAnimation);
// 安全超时:若动画或跳转异常,最多等待 SAFETY_TIMEOUT_MS 后强制进入主界面
mHandler.postDelayed(this::goToMain, SAFETY_TIMEOUT_MS);
}
private void startEnterAnimation() {
if (mEnterAnimationStarted || isFinishing()) return;
if (mLogo == null || mTitle == null || mAccent == null) {
goToMain();
return;
}
mEnterAnimationStarted = true;
// 1. Logo缩放 + 淡入
mLogo.setScaleX(0.82f);
mLogo.setScaleY(0.82f);
mLogo.setAlpha(0f);
ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogo, View.SCALE_X, 0.82f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogo, View.SCALE_Y, 0.82f, 1f);
ObjectAnimator alphaLogo = ObjectAnimator.ofFloat(mLogo, View.ALPHA, 0f, 1f);
scaleX.setDuration(520);
scaleY.setDuration(520);
alphaLogo.setDuration(420);
AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
scaleX.setInterpolator(interpolator);
scaleY.setInterpolator(interpolator);
alphaLogo.setInterpolator(interpolator);
scaleX.start();
scaleY.start();
alphaLogo.start();
// 2. 标题:延迟淡入 + 上移
mTitle.setAlpha(0f);
mTitle.setTranslationY(12f);
mTitle.animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(280)
.setDuration(400)
.setInterpolator(interpolator)
.start();
// 3. 底部细线:淡入
mAccent.setAlpha(0f);
mAccent.animate()
.alpha(1f)
.setStartDelay(460)
.setDuration(320)
.setInterpolator(new LinearInterpolator())
.start();
// 4. 停留后退场并跳转
mHandler.postDelayed(this::startExitAnimation, PHASE_ENTER_MS + HOLD_MS);
}
private void startExitAnimation() {
if (isFinishing() || mGoMainCalled) return;
if (mContentRoot == null) {
goToMain();
return;
}
mContentRoot.animate()
.alpha(0f)
.setDuration(PHASE_EXIT_MS)
.setInterpolator(new AccelerateDecelerateInterpolator())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (mContentRoot != null) {
mContentRoot.animate().setListener(null);
}
goToMain();
}
})
.start();
}
private void goToMain() {
if (mGoMainCalled) return;
mGoMainCalled = true;
mHandler.removeCallbacksAndMessages(null);
if (isFinishing()) return;
try {
startActivity(new Intent(this, MainActivity.class));
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
} catch (Exception e) {
overridePendingTransition(0, 0);
}
finish();
}
@Override
protected void onDestroy() {
mHandler.removeCallbacksAndMessages(null);
mContentRoot = null;
mLogo = null;
mTitle = null;
mAccent = null;
super.onDestroy();
}
}

@ -1,215 +0,0 @@
package net.micode.notes.ui;
/**
* /
* <p>
* DatePicker + TimePickerGeofence
* TodoReminderManager AlarmManager/Geofencing
* </p>
*
* @see TodoItem
* @see TodoReminderManager
* @see TodoDao
*/
import android.Manifest;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import android.widget.EditText;
import net.micode.notes.R;
import net.micode.notes.data.TodoDao;
import net.micode.notes.todo.TodoItem;
import net.micode.notes.todo.TodoReminderManager;
import java.util.Calendar;
public class TodoEditActivity extends AppCompatActivity {
private static final String EXTRA_TODO_ID = "todo_id";
private EditText mEtContent;
private Button mBtnAddReminder;
private TextView mTvReminderInfo;
private Button mBtnSave;
private TodoDao mTodoDao;
private TodoItem mItem;
private boolean mIsEdit;
private final ActivityResultLauncher<String[]> mLocationPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) {
applyMockLocation();
} else {
Toast.makeText(this, "需要位置权限才能设置地点提醒", Toast.LENGTH_SHORT).show();
}
});
public static Intent newIntent(Context context) {
return new Intent(context, TodoEditActivity.class);
}
public static Intent newIntent(Context context, long todoId) {
Intent i = new Intent(context, TodoEditActivity.class);
i.putExtra(EXTRA_TODO_ID, todoId);
return i;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_todo_edit);
if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mTodoDao = new TodoDao(this);
Intent intent = getIntent();
long todoId = (intent != null) ? intent.getLongExtra(EXTRA_TODO_ID, 0) : 0;
mIsEdit = todoId > 0;
mItem = mIsEdit ? mTodoDao.getById(todoId) : new TodoItem();
if (mItem == null) mItem = new TodoItem();
mEtContent = findViewById(R.id.et_todo_content);
mBtnAddReminder = findViewById(R.id.btn_add_reminder);
mTvReminderInfo = findViewById(R.id.tv_reminder_info);
mBtnSave = findViewById(R.id.btn_save);
if (mIsEdit) mEtContent.setText(mItem.getContent());
updateReminderInfo();
mBtnAddReminder.setOnClickListener(v -> showReminderOptions());
mBtnSave.setOnClickListener(v -> save());
}
@Override
public boolean onSupportNavigateUp() {
finish();
return true;
}
private void showReminderOptions() {
String[] options = new String[]{
getString(R.string.todo_reminder_time),
getString(R.string.todo_reminder_location),
getString(R.string.todo_reminder_none)
};
new android.app.AlertDialog.Builder(this)
.setTitle(R.string.todo_add_reminder)
.setItems(options, (dialog, which) -> {
if (which == 0) pickTime();
else if (which == 1) pickLocation();
else clearReminder();
})
.show();
}
private void clearReminder() {
mItem.setReminderType(TodoItem.REMINDER_NONE);
mItem.setReminderTimestamp(0);
mItem.setLatitude(0);
mItem.setLongitude(0);
mItem.setLocationName("");
updateReminderInfo();
}
private void pickTime() {
Calendar cal = Calendar.getInstance();
if (mItem.getReminderTimestamp() > 0) cal.setTimeInMillis(mItem.getReminderTimestamp());
DatePickerDialog dateDialog = new DatePickerDialog(this,
(v, year, month, dayOfMonth) -> {
cal.set(year, month, dayOfMonth);
TimePickerDialog timeDialog = new TimePickerDialog(this,
(tv, hour, minute) -> {
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
mItem.setReminderType(TodoItem.REMINDER_TIME);
mItem.setReminderTimestamp(cal.getTimeInMillis());
mItem.setLatitude(0);
mItem.setLongitude(0);
mItem.setLocationName("");
updateReminderInfo();
},
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE),
DateFormat.is24HourFormat(this));
timeDialog.show();
},
cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
dateDialog.show();
}
private void pickLocation() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mLocationPermissionLauncher.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
});
} else {
mLocationPermissionLauncher.launch(new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
}
} else {
applyMockLocation();
}
}
private void applyMockLocation() {
// 模拟选点:返回虚拟坐标和地点名称(无真实地图 SDK
mItem.setReminderType(TodoItem.REMINDER_LOCATION);
mItem.setReminderTimestamp(0);
mItem.setLatitude(39.9042);
mItem.setLongitude(116.4074);
mItem.setLocationName("附近超市");
updateReminderInfo();
Toast.makeText(this, "已设置地点提醒:附近超市", Toast.LENGTH_SHORT).show();
}
private void updateReminderInfo() {
if (mItem.getReminderType() == TodoItem.REMINDER_TIME && mItem.getReminderTimestamp() > 0) {
mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
mTvReminderInfo.setText("时间提醒: " + DateFormat.format("yyyy-MM-dd HH:mm", mItem.getReminderTimestamp()));
} else if (mItem.getReminderType() == TodoItem.REMINDER_LOCATION) {
mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
mTvReminderInfo.setText("地点提醒: " + mItem.getLocationName());
} else {
mTvReminderInfo.setVisibility(android.view.View.GONE);
}
}
private void save() {
String content = mEtContent.getText() != null ? mEtContent.getText().toString().trim() : "";
if (TextUtils.isEmpty(content)) {
Toast.makeText(this, "请输入待办内容", Toast.LENGTH_SHORT).show();
return;
}
mItem.setContent(content);
if (mItem.getCreatedTime() == 0) mItem.setCreatedTime(System.currentTimeMillis());
if (mIsEdit && mItem.getId() > 0) {
mTodoDao.update(mItem);
} else {
long id = mTodoDao.insert(mItem);
mItem.setId(id);
}
TodoReminderManager.scheduleReminders(this, mItem);
setResult(RESULT_OK);
finish();
}
}

@ -1,238 +0,0 @@
package net.micode.notes.ui;
/**
* Fragment
* <p>
* /
*
*
* </p>
*
* @see TodoAdapter
* @see TodoEditActivity
* @see TodoReminderManager
*/
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import net.micode.notes.R;
import net.micode.notes.data.TodoDao;
import net.micode.notes.todo.TodoReminderManager;
import net.micode.notes.todo.ConfettiView;
import net.micode.notes.todo.TodoAdapter;
import net.micode.notes.todo.TodoItem;
import java.util.ArrayList;
import java.util.List;
public class TodoFragment extends Fragment {
private static final int REQUEST_EDIT = 2001;
private RecyclerView mRecyclerView;
private TodoAdapter mAdapter;
private FloatingActionButton mFab;
private MaterialToolbar mToolbar;
private ConfettiView mConfettiView;
private TodoDao mTodoDao;
private List<TodoItem> mUndoneList = new ArrayList<>();
private List<TodoItem> mDoneList = new ArrayList<>();
private ActionModeCallback mActionModeCallback;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_todo, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mTodoDao = new TodoDao(requireContext());
mRecyclerView = view.findViewById(R.id.recycler_todo);
mFab = view.findViewById(R.id.fab_add_todo);
mToolbar = view.findViewById(R.id.toolbar_todo);
mConfettiView = view.findViewById(R.id.confetti_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
mAdapter = new TodoAdapter();
mRecyclerView.setAdapter(mAdapter);
mAdapter.setOnItemClickListener(new TodoAdapter.OnItemClickListener() {
@Override
public void onItemClick(TodoItem item, int position) {
if (!mAdapter.isChoiceMode()) {
Intent intent = TodoEditActivity.newIntent(requireContext(), item.getId());
startActivityForResult(intent, REQUEST_EDIT);
}
}
@Override
public void onCheckChanged(TodoItem item, boolean checked) {
if (mAdapter.isChoiceMode()) return;
if (item == null || item.getId() <= 0) return;
item.setDone(checked);
mTodoDao.update(item);
refreshData();
if (checked) {
checkAndTriggerGamification();
}
}
});
mAdapter.setOnItemLongClickListener((item, position) -> {
if (!mAdapter.isChoiceMode() && mActionModeCallback == null) {
mActionModeCallback = new ActionModeCallback();
requireActivity().startActionMode(mActionModeCallback);
mAdapter.setChoiceMode(true);
mAdapter.setChecked(position, true);
mFab.setVisibility(View.GONE);
}
});
mFab.setOnClickListener(v -> {
if (getActivity() == null || !isAdded()) return;
Intent intent = new Intent(getActivity(), TodoEditActivity.class);
startActivityForResult(intent, REQUEST_EDIT);
});
if (mToolbar != null && getActivity() instanceof MainActivity) {
mToolbar.setNavigationOnClickListener(v -> ((MainActivity) getActivity()).openDrawer());
}
mConfettiView.setOnClickListener(v -> mConfettiView.setVisibility(View.GONE));
refreshData();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_EDIT && resultCode == Activity.RESULT_OK) {
refreshData();
}
}
private void refreshData() {
mUndoneList = mTodoDao.getUndone();
mDoneList = mTodoDao.getDone();
mAdapter.setData(mUndoneList, mDoneList);
}
private void checkAndTriggerGamification() {
int undoneCount = mTodoDao.getUndoneCount();
if (undoneCount == 0) {
triggerGamification();
}
}
private void triggerGamification() {
vibrate();
playSuccessSound();
showConfetti();
}
private void vibrate() {
try {
if (getContext() == null) return;
Vibrator v = (Vibrator) getContext().getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (v != null && v.hasVibrator()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE));
} else {
v.vibrate(150);
}
}
} catch (Exception e) { /* ignore */ }
}
/** 撒花时的短促有力提示音(系统 ToneGenerator无需 raw 资源) */
private static final int SUCCESS_TONE_DURATION_MS = 120;
private void playSuccessSound() {
try {
Context ctx = getContext();
if (ctx == null) return;
ToneGenerator tone = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 85);
tone.startTone(ToneGenerator.TONE_PROP_ACK, SUCCESS_TONE_DURATION_MS);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
tone.release();
} catch (Exception ignored) { }
}, SUCCESS_TONE_DURATION_MS + 80);
} catch (Exception e) { /* 无扬声器或权限时静默 */ }
}
private void showConfetti() {
if (mConfettiView != null) {
mConfettiView.post(() -> {
mConfettiView.start();
});
}
}
private class ActionModeCallback implements android.view.ActionMode.Callback {
@Override
public boolean onCreateActionMode(android.view.ActionMode mode, android.view.Menu menu) {
requireActivity().getMenuInflater().inflate(R.menu.todo_list_options, menu);
return true;
}
@Override
public boolean onPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(android.view.ActionMode mode, android.view.MenuItem item) {
if (item.getItemId() == R.id.delete) {
List<Long> ids = mAdapter.getSelectedIds();
if (ids.isEmpty()) {
Toast.makeText(requireContext(), R.string.menu_select_none, Toast.LENGTH_SHORT).show();
return true;
}
new android.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.alert_title_delete)
.setMessage(getString(R.string.todo_delete_confirm, ids.size()))
.setPositiveButton(android.R.string.ok, (d, w) -> {
for (long id : ids) TodoReminderManager.cancelAllForTodo(requireContext(), id);
mTodoDao.deleteByIds(ids);
refreshData();
mode.finish();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
return true;
}
@Override
public void onDestroyActionMode(android.view.ActionMode mode) {
mAdapter.setChoiceMode(false);
mFab.setVisibility(View.VISIBLE);
mActionModeCallback = null;
}
}
}

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/bottom_nav_selected" android:state_checked="true" />
<item android:color="@color/bottom_nav_unselected" />
</selector>

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#CC3370FF"/> <!-- 半透明蓝色 (CC是透明度) -->
<stroke android:width="1dp" android:color="#FFFFFF"/> <!-- 白色描边 -->
</shape>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 退出图标,用于退出隐私空间 -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1D1D1D"
android:pathData="M3,18h18v-2H3v2zM3,13h18v-2H3v2zM3,6v2h18V6H3z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M19.14,12.94c0.04,-0.31 0.06,-0.63 0.06,-0.94c0,-0.31 -0.02,-0.63 -0.06,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.31 4.8,11.63 4.8,12s0.02,0.69 0.06,1.06l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#5C5C5C"
android:pathData="M12,4V1L8,5l4,4V6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
</vector>

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

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="160"
android:startColor="#0D0D0D"
android:centerColor="#151520"
android:endColor="#0D0D0D"
android:type="linear" />
</shape>

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="@color/surface_white">
<!-- 主内容区 -->
<LinearLayout
android:id="@+id/main_content_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/surface_white">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_white"
app:itemIconTint="@color/bottom_nav_color"
app:itemTextColor="@color/bottom_nav_color"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>
<!-- 侧边栏 -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
android:background="@color/surface_white"
app:headerLayout="@layout/drawer_header"
app:itemIconTint="@color/drawer_icon_tint"
app:itemTextColor="@color/drawer_text_primary"
app:menu="@menu/drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/splash_background"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/splash_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:orientation="vertical">
<!-- Logo简约图标 -->
<ImageView
android:id="@+id/splash_logo"
android:layout_width="88dp"
android:layout_height="88dp"
android:alpha="0"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/icon_app" />
<View
android:layout_width="match_parent"
android:layout_height="24dp" />
<!-- App 名称:细字重、字间距 -->
<TextView
android:id="@+id/splash_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0"
android:letterSpacing="0.15"
android:text="@string/app_name"
android:textColor="#E8E8E8"
android:textSize="22sp"
android:textStyle="normal"
android:fontFamily="sans-serif-light" />
<View
android:layout_width="match_parent"
android:layout_height="12dp" />
<!-- 副标题:极简点缀 -->
<View
android:id="@+id/splash_accent"
android:layout_width="32dp"
android:layout_height="2dp"
android:alpha="0"
android:background="@drawable/splash_accent_line" />
</LinearLayout>
</FrameLayout>

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_light"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/et_todo_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minLines="3"
android:gravity="top"
android:hint="@string/todo_hint_content"
android:inputType="textMultiLine|textCapSentences"
android:background="@android:drawable/editbox_background"
android:padding="12dp" />
<Button
android:id="@+id/btn_add_reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/todo_add_reminder" />
<TextView
android:id="@+id/tv_reminder_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:visibility="gone" />
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/todo_save" />
</LinearLayout>
</ScrollView>

@ -1,27 +0,0 @@
<?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:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="32dp"
android:paddingBottom="24dp"
android:background="@color/surface_white">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="22sp"
android:textStyle="bold"
android:textColor="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="简约 · 高效"
android:textSize="14sp"
android:textColor="@color/text_secondary" />
</LinearLayout>

@ -1,45 +0,0 @@
<?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="match_parent"
android:orientation="vertical"
android:background="#FFFFFF">
<!-- 顶部控制区 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#F5F6F7"
android:paddingHorizontal="16dp"
android:elevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="💡 灵感箱"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"/>
<!-- 悬浮球开关 -->
<Switch
android:id="@+id/switch_floating_ball"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="开启桌面悬浮球 "
android:textSize="14sp"/>
</RelativeLayout>
<!-- 灵感便签列表 -->
<!-- 复用已有的 List View 逻辑 -->
<ListView
android:id="@+id/inspiration_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:dividerHeight="0dp"/>
</LinearLayout>

@ -1,122 +0,0 @@
<?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="@color/background_light">
<!-- 顶部标题栏 -->
<RelativeLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/surface_white"
android:elevation="2dp">
<!-- 左侧:新对话按钮 -->
<ImageView
android:id="@+id/btn_new_chat"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="8dp"
android:padding="8dp"
android:src="@android:drawable/ic_menu_rotate" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Mi 管家"
android:textStyle="bold"
android:textSize="16sp"/>
<!-- 右侧:灵感球 Switch -->
<Switch
android:id="@+id/switch_steward_floating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="10dp"
android:text="灵感球"
android:textSize="12sp" />
</RelativeLayout>
<!-- 中间的 Logo 区域 (初始显示) -->
<LinearLayout
android:id="@+id/center_logo_area"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@android:drawable/sym_def_app_icon"/> <!-- 可替换为自定义Logo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="你的私人管家"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@color/text_primary"/>
</LinearLayout>
<!-- 聊天列表 (初始隐藏) -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/top_bar"
android:layout_above="@id/bottom_input_area"
android:visibility="gone"
android:clipToPadding="false"
android:paddingBottom="10dp"/>
<!-- 底部输入区域 -->
<LinearLayout
android:id="@+id/bottom_input_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:padding="12dp"
android:background="@color/surface_white"
android:orientation="vertical">
<CheckBox
android:id="@+id/cb_use_knowledge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用知识库"
android:checked="true"
android:textSize="12sp"
android:textColor="@color/text_secondary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/et_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:drawable/editbox_background"
android:hint="问点什么..."
android:padding="10dp"
android:minHeight="48dp"/>
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"
android:layout_marginLeft="8dp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_light">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface_white"
android:elevation="2dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_todo"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/todo_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_todo"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:clipToPadding="false"
android:paddingBottom="80dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_todo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="24dp"
app:srcCompat="@drawable/ic_add_24dp"
app:tint="@android:color/white" />
<net.micode.notes.todo.ConfettiView
android:id="@+id/confetti_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
android:background="#40000000" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,82 +0,0 @@
<?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="horizontal"
android:padding="16dp">
<!-- AI 头像 -->
<ImageView
android:layout_width="36dp"
android:layout_height="36dp"
android:src="@android:drawable/sym_def_app_icon"
android:layout_marginRight="12dp"
android:layout_marginTop="2dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 思考状态 -->
<LinearLayout
android:id="@+id/layout_thinking"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:visibility="visible">
<ProgressBar
android:layout_width="16dp"
android:layout_height="16dp"
style="?android:attr/progressBarStyleSmall"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" 深度思考中..."
android:textSize="12sp"
android:textColor="#999999"/>
</LinearLayout>
<!-- 正文内容 -->
<TextView
android:id="@+id/tv_ai_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是AI回复的内容"
android:textColor="#333333"
android:textSize="16sp"
android:lineSpacingMultiplier="1.2"
android:visibility="gone"/>
<!-- 参考资料区域 -->
<LinearLayout
android:id="@+id/layout_references"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="12dp"
android:background="#F0F0F0"
android:padding="8dp"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="参考便签:"
android:textSize="10sp"
android:textStyle="bold"
android:textColor="#666666"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/tv_reference_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1. 工作安排\n2. 会议纪要"
android:textSize="12sp"
android:textColor="#3370FF"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -1,17 +0,0 @@
<?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:gravity="right"
android:padding="16dp">
<TextView
android:id="@+id/tv_user_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="40dp"
android:text="帮我查一下明天的日程"
android:textColor="#000000"
android:textSize="22sp"
android:textStyle="bold" />
</LinearLayout>

@ -1,30 +0,0 @@
<?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="horizontal"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:minHeight="56dp"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/cb_todo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp" />
<TextView
android:id="@+id/tv_todo_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@color/text_primary"
android:ellipsize="end"
android:maxLines="3"
android:lineSpacingExtra="2dp" />
</LinearLayout>

@ -1,24 +0,0 @@
<?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:background="@color/background_light">
<View
android:layout_width="match_parent"
android:layout_height="8dp"
android:background="@color/divider_light" />
<TextView
android:id="@+id/tv_section_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:textSize="14sp"
android:textColor="@color/text_secondary"
android:textStyle="bold" />
</LinearLayout>

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- 悬浮球主体:圆形背景 + 图标 -->
<ImageView
android:id="@+id/iv_floating_ball"
android:layout_width="56dp"
android:layout_height="56dp"
android:src="@android:drawable/ic_menu_edit"
android:background="@drawable/bg_floating_ball"
android:padding="12dp"
android:contentDescription="灵感球"
android:elevation="6dp"/>
<!-- elevation 提供阴影效果 -->
</FrameLayout>

@ -1,43 +0,0 @@
<?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="#99000000"> <!-- 半透明黑背景 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="32dp"
android:background="@android:drawable/dialog_holo_light_frame"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="捕捉灵感"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<EditText
android:id="@+id/et_inspiration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入此刻的想法..."
android:minLines="2"
android:gravity="top"
android:background="@android:drawable/edit_text">
<!-- 标准的自动聚焦写法 -->
<requestFocus />
</EditText>
<Button
android:id="@+id/btn_send_inspiration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="8dp"
android:text="存入灵感箱"/>
</LinearLayout>
</RelativeLayout>

@ -1,108 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_light"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 顶栏:菜单按钮 + 标题 -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_notes"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/surface_white"
android:elevation="0dp"
app:navigationIcon="@drawable/ic_menu_24dp"
app:title="@string/notes_tab_title"
app:titleTextColor="@color/text_primary"
app:titleTextAppearance="@style/ToolbarTitleTextAppearance" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light" />
<!-- 子文件夹时显示的标题栏(与原有逻辑兼容) -->
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:background="@color/surface_white"
android:gravity="center_vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:singleLine="true"
android:textColor="@color/text_primary"
android:textSize="16sp" />
<ListView
android:id="@+id/notes_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:cacheColorHint="@android:color/transparent"
android:listSelector="@android:color/transparent"
android:divider="@color/divider_light"
android:dividerHeight="1dp"
android:background="@color/background_light"
android:fadingEdge="none"
android:scrollbars="none" />
</LinearLayout>
<!-- 右下角:返回上一级(仅子文件夹内显示)+ 新建便签 + 退出隐私空间(仅隐私模式下显示) -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="84dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/fab_back_to_list"
android:visibility="gone"
app:srcCompat="@drawable/ic_arrow_back_24dp"
app:backgroundTint="@color/text_secondary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_exit_privacy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="84dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/privacy_space_exit"
android:visibility="gone"
app:srcCompat="@drawable/ic_exit_privacy_24dp"
app:backgroundTint="@color/text_secondary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_fab_new_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="20dp"
android:layout_marginBottom="88dp"
android:contentDescription="@string/notelist_menu_new"
app:srcCompat="@drawable/ic_add_24dp"
app:backgroundTint="@color/accent_primary"
app:tint="@color/surface_white"
app:elevation="6dp"
app:fabSize="normal"
app:maxImageSize="24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_mi_steward"
android:icon="@android:drawable/ic_dialog_info"
android:title="@string/nav_mi_steward" />
<item
android:id="@+id/nav_notes"
android:icon="@android:drawable/ic_menu_edit"
android:title="@string/nav_notes" />
<item
android:id="@+id/nav_todo"
android:icon="@android:drawable/ic_menu_agenda"
android:title="@string/nav_todo" />
</menu>

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/drawer_new_note"
android:icon="@drawable/ic_edit_note_24dp"
android:title="@string/notelist_menu_new" />
<item
android:id="@+id/drawer_new_folder"
android:icon="@drawable/ic_folder_24dp"
android:title="@string/menu_create_folder" />
<item
android:id="@+id/drawer_export"
android:icon="@drawable/ic_export_24dp"
android:title="@string/menu_export_text" />
</menu>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
<item
android:id="@+id/menu_setting"
android:title="@string/menu_setting"/>
</menu>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/delete"
android:title="@string/todo_delete"
android:icon="@drawable/menu_delete"
android:showAsAction="always|withText" />
</menu>

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<!-- 置顶条加深色:与便签五色对应,用于列表项左侧标识置顶 -->
<color name="note_pinned_bar_yellow">#CCB8860B</color>
<color name="note_pinned_bar_blue">#CC1565C0</color>
<color name="note_pinned_bar_white">#CC616161</color>
<color name="note_pinned_bar_green">#CC2E7D32</color>
<color name="note_pinned_bar_red">#CCC62828</color>
<!-- 简约白主题 -->
<color name="surface_white">#FFFFFF</color>
<color name="background_light">#F7F8FA</color>
<color name="divider_light">#EFF1F3</color>
<color name="text_primary">#1D1D1D</color>
<color name="text_secondary">#657786</color>
<color name="accent_primary">#1DA1F2</color>
<color name="accent_primary_dark">#1A91DA</color>
<!-- 底部导航与侧边栏 -->
<color name="bottom_nav_unselected">#657786</color>
<color name="bottom_nav_selected">#1DA1F2</color>
<color name="drawer_icon_tint">#657786</color>
<color name="drawer_text_primary">#1D1D1D</color>
<!-- 隐私空间:整体变暗背景 -->
<color name="privacy_space_background">#FF1C1C1E</color>
</resources>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 代表 Context.getExternalFilesDir() 下的子目录,供 FileProvider 生成 content:// URI -->
<external-files-path name="my_images" path="images/" />
<!-- 相机拍摄照片保存在 getExternalFilesDir(Environment.DIRECTORY_PICTURES) 即 files/Pictures/ -->
<external-files-path name="my_pictures" path="Pictures/" />
</paths>

@ -1,17 +0,0 @@
package net.micode.notes;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

@ -1,4 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}

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

Loading…
Cancel
Save