Compare commits

..

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

Binary file not shown.

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 按下状态:灰色 -->
<item android:state_pressed="true">
<shape>
<solid android:color="#E0E0E0" />
</shape>
</item>
<!-- 默认状态:透明 -->
<item>
<shape>
<solid android:color="@android:color/transparent" />
</shape>
</item>
</selector>

@ -1,5 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<corners android:topLeftRadius="2dp" android:topRightRadius="12dp"
android:bottomRightRadius="12dp" android:bottomLeftRadius="12dp" />
</shape>

@ -1,5 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#E3F2FD" /> <!-- 浅蓝色背景 -->
<stroke android:width="1dp" android:color="#2196F3" /> <!-- 深蓝边框 -->
<corners android:radius="12dp" />
</shape>

@ -1,5 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFD700" /> <!-- 使用小米金色 -->
<corners android:topLeftRadius="12dp" android:topRightRadius="2dp"
android:bottomRightRadius="12dp" android:bottomLeftRadius="12dp" />
</shape>

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#80000000" /> <!-- 半透明黑色 -->
<corners android:radius="12dp" /> <!-- 圆角 -->
<padding
android:left="8dp"
android:top="4dp"
android:right="8dp"
android:bottom="4dp" />
</shape>

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

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 第一层:绿色背景方框 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#4CAF50" />
<corners android:radius="4dp" />
</shape>
</item>
<!-- 第二层:白色对勾 (使用系统自带的对勾图案) -->
<item
android:drawable="@android:drawable/checkbox_on_background"
android:top="2dp"
android:bottom="2dp"
android:left="2dp"
android:right="2dp" />
</layer-list>

@ -1,15 +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="@android:color/transparent" />
<!-- 边框颜色和粗细 -->
<stroke
android:width="2dp"
android:color="#BDBDBD" />
<!-- 圆角处理 -->
<corners android:radius="4dp" />
<size
android:width="20dp"
android:height="20dp" />
</shape>

@ -1,64 +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:padding="32dp"
android:gravity="center_vertical"
android:background="@color/main_background_gray">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/icon_app"
android:layout_marginBottom="32dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="小米便签"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/primary_text_black"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="48dp" />
<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="56dp"
android:hint="电子邮箱"
android:inputType="textEmailAddress"
android:paddingLeft="16dp"
android:background="@android:drawable/editbox_background_normal"
android:layout_marginBottom="16dp" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="56dp"
android:hint="密码"
android:inputType="textPassword"
android:paddingLeft="16dp"
android:background="@android:drawable/editbox_background_normal"
android:layout_marginBottom="24dp" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="登录 / 注册"
android:backgroundTint="@color/mi_gold"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ProgressBar
android:id="@+id/loading_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:visibility="gone" />
</LinearLayout>

@ -1,57 +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:padding="12dp"
android:background="@drawable/bg_agenda_item_selector"> <!-- 需定义点击波纹 -->
<!-- 左侧 1/4 区域:清单符 + 时间 -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.2"
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 清单符 -->
<ImageView
android:id="@+id/iv_agenda_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/checkbox_unchecked" /> <!-- 需准备白色方块和绿色对勾图标 -->
<!-- 时间标签 -->
<TextView
android:id="@+id/tv_agenda_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="16:00"
android:textSize="14sp"
android:textColor="#666666"
android:textStyle="bold"/>
</LinearLayout>
<!-- 右侧 3/4 区域:事项内容 -->
<TextView
android:id="@+id/tv_agenda_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:text="去机场接导师"
android:textSize="16sp"
android:textColor="#333333"
android:paddingLeft="12dp"/>
<!-- 删除按钮 (默认隐藏,侧滑或长按显示,这里简单起见放最右侧) -->
<ImageView
android:id="@+id/btn_agenda_delete"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_delete"
android:alpha="0.3"
android:padding="4dp"/>
</LinearLayout>

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp">
<ImageView
android:id="@+id/iv_floating_ball"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/icon_app"
android:background="@android:drawable/editbox_background_normal"
android:elevation="10dp"
android:contentDescription="Quick Note" />
</FrameLayout>

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_folder_tab_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginRight="10dp"
android:background="@drawable/bg_folder_tab_selector"
android:gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="@color/primary_text_black"
android:textSize="14sp"
android:clickable="true"
android:focusable="true" />

@ -1,116 +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">
<!-- 1. 日历 -->
<CalendarView
android:id="@+id/calendar_view"
android:layout_width="match_parent"
android:layout_height="320dp"
android:background="#F9F9F9" />
<!-- 2. 日期标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="#F0F0F0"
android:gravity="center_vertical"
android:orientation="horizontal">
<View android:layout_width="4dp" android:layout_height="24dp" android:background="@color/mi_gold" />
<TextView
android:id="@+id/tv_agenda_date_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="10dp"
android:textStyle="bold"
android:textColor="#333333"
android:gravity="center_vertical"/>
<!-- [新增] 切换按钮 -->
<ImageButton
android:id="@+id/btn_toggle_calendar"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/arrow_up_float"
android:contentDescription="收起日历" />
</LinearLayout>
<!-- 3. 列表区域 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/agenda_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- [核心 ID] 空状态容器 -->
<LinearLayout
android:id="@+id/ll_empty_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@android:drawable/ic_menu_today"
android:alpha="0.2" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无日程安排"
android:textColor="#CCCCCC" />
</LinearLayout>
</FrameLayout>
<!-- 4. 快速添加栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="#F5F5F5"
android:gravity="center_vertical"
android:orientation="horizontal">
<!-- [新增] 时间选择按钮 -->
<TextView
android:id="@+id/tv_set_quick_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="全天"
android:padding="8dp"
android:textColor="@color/mi_gold"
android:textStyle="bold"
android:background="?attr/selectableItemBackground" />
<EditText
android:id="@+id/et_quick_add"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="快速添加今日事项..."
android:textSize="14sp"
android:background="@null"
android:padding="10dp"/>
<Button
android:id="@+id/btn_quick_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加"
style="?android:attr/borderlessButtonStyle" />
</LinearLayout>
</LinearLayout>

@ -1,44 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EDEDED"> <!-- 微信经典的浅灰背景 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_chat_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/input_layout"
android:clipToPadding="false"
android:padding="10dp" />
<LinearLayout
android:id="@+id/input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="#F7F7F7"
android:elevation="4dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="8dp">
<EditText
android:id="@+id/et_chat_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:drawable/editbox_background_normal"
android:hint="向 AI 助理提问..."
android:maxLines="4"
android:padding="10dp" />
<Button
android:id="@+id/btn_chat_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:backgroundTint="#FFD700"
android:text="发送"
android:textColor="#000000" />
</LinearLayout>
</RelativeLayout>

@ -1,64 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<!-- 【新增手术位置:最顶部】 -->
<TextView
android:id="@+id/tv_chat_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:background="#D0D0D0"
android:textColor="#FFFFFF"
android:textSize="10sp"
android:visibility="gone" />
<!-- 1. 消息容器 -->
<RelativeLayout
android:id="@+id/rl_msg_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 左侧气泡容器 (AI & Reminder) -->
<LinearLayout
android:id="@+id/ll_left_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginRight="60dp">
<TextView
android:id="@+id/tv_msg_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_bubble_ai"
android:padding="12dp"
android:textColor="#000000"
android:textSize="16sp" />
</LinearLayout>
<!-- 右侧气泡容器 (User) -->
<LinearLayout
android:id="@+id/ll_right_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="60dp">
<TextView
android:id="@+id/tv_msg_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_bubble_user"
android:padding="12dp"
android:textColor="#000000"
android:textSize="16sp" />
</LinearLayout>
</RelativeLayout>
</LinearLayout>

@ -1,140 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/note_edit_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
<!-- 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.
-->
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/main_background_gray"
app:titleTextColor="@color/primary_text_black"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<LinearLayout
android:id="@+id/note_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="8dp">
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_modified_date"
android:layout_width="0dp"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="left|center_vertical"
android:layout_marginRight="8dip"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/title_alert"
android:visibility="gone" />
android:layout_gravity="center_vertical"
android:background="@drawable/title_alert" />
<TextView
android:id="@+id/tv_alert_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:layout_marginRight="8dp"
android:textAppearance="@style/TextAppearanceSecondaryItem"
android:visibility="gone" />
<!-- 新增:撤销按钮 -->
<ImageView
android:id="@+id/btn_undo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_revert"
android:padding="8dp"
android:alpha="0.3"
android:clickable="false" />
<!-- 新增:重做按钮 -->
<ImageView
android:id="@+id/btn_redo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_revert"
android:scaleX="-1"
android:padding="8dp"
android:alpha="0.3"
android:clickable="false" />
android:layout_gravity="center_vertical"
android:layout_marginLeft="2dip"
android:layout_marginRight="8dip"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
<ImageView
android:id="@+id/btn_set_bg_color"
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/bg_btn_set_color"
android:padding="5dp"/>
android:layout_gravity="center"
android:background="@drawable/bg_btn_set_color" />
</LinearLayout>
<LinearLayout
android:id="@+id/sv_note_edit"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<ImageView
android:layout_width="match_parent"
android:layout_height="7dp"
android:background="@drawable/bg_color_btn_mask"
android:visibility="gone" />
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
<ScrollView
android:id="@+id/note_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never">
android:overScrollMode="never"
android:layout_gravity="left|top"
android:fadingEdgeLength="0dip">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- [新增] 图片附件栏容器 -->
<!-- 放在 NoteEditText 之前,确保它能随页面滚动 -->
<HorizontalScrollView
android:id="@+id/attachment_bar_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:clipToPadding="false"
android:scrollbars="none">
<LinearLayout
android:id="@+id/attachment_bar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<!-- 图片将动态添加到这里 -->
</LinearLayout>
</HorizontalScrollView>
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="top|left"
android:gravity="left|top"
android:background="@null"
android:autoLink="none"
android:autoLink="all"
android:linksClickable="false"
android:minLines="12"
android:textAppearance="@style/TextAppearancePrimaryItem"
@ -142,246 +101,145 @@
<LinearLayout
android:id="@+id/note_edit_list"
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginLeft="-10dp"
android:layout_marginLeft="-10dip"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
<!-- [位置确认] 工具栏必须在 ScrollView 之外LinearLayout 之内 -->
<LinearLayout
android:id="@+id/bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#F5F5F5"
android:orientation="horizontal"
android:gravity="center_vertical"
android:elevation="8dp">
<ImageButton
android:id="@+id/btn_insert_image"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_gallery" /> <!-- 改用你刚生成的本地图标 -->
<ImageButton
android:id="@+id/btn_toggle_list"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/checkbox_off_background" />
<ImageButton
android:id="@+id/btn_format_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit" />
</LinearLayout>
<!-- 在原有 bottom_toolbar 同级位置添加 -->
<HorizontalScrollView
android:id="@+id/format_toolbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#F8F8F8"
android:visibility="gone"
android:scrollbars="none"
android:elevation="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<!-- 字体大小组 -->
<Button android:id="@+id/btn_font_small" android:layout_width="50dp" android:layout_height="wrap_content" android:text="S" android:textColor="#333333" android:textSize="10sp" style="?android:attr/borderlessButtonStyle"/>
<Button android:id="@+id/btn_font_normal" android:layout_width="50dp" android:layout_height="wrap_content" android:text="M" android:textColor="#333333" android:textSize="14sp" style="?android:attr/borderlessButtonStyle"/>
<Button android:id="@+id/btn_font_large" android:layout_width="50dp" android:layout_height="wrap_content" android:text="L" android:textColor="#333333" android:textSize="18sp" style="?android:attr/borderlessButtonStyle"/>
<Button android:id="@+id/btn_font_super" android:layout_width="50dp" android:layout_height="wrap_content" android:text="XL" android:textColor="#333333" android:textSize="22sp" style="?android:attr/borderlessButtonStyle"/>
<View android:layout_width="1dp" android:layout_height="30dp" android:background="#DDD"/>
<!-- 样式组 -->
<ImageButton android:id="@+id/btn_bold" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_edit" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="加粗"/>
<ImageButton android:id="@+id/btn_italic" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_compass" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="斜体"/>
<ImageButton android:id="@+id/btn_underline" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_sort_alphabetically" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="下划线"/>
<ImageButton android:id="@+id/btn_strike" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_close_clear_cancel" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="中划线/清除"/>
<ImageButton android:id="@+id/btn_highlight" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_view" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="高亮"/>
<ImageButton android:id="@+id/btn_color_toggle" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/button_onoff_indicator_on" android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="切换黑白颜色"/>
<View android:layout_width="1dp" android:layout_height="30dp" android:background="#DDD"/>
<!-- 关闭按钮 -->
<ImageButton android:id="@+id/btn_close_format" android:layout_width="48dp" android:layout_height="48dp" android:src="@android:drawable/ic_menu_close_clear_cancel" android:background="?attr/selectableItemBackgroundBorderless"/>
</LinearLayout>
</HorizontalScrollView>
<ImageView
android:layout_width="match_parent"
android:layout_height="7dp"
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/btn_set_bg_color"
android:layout_height="43dip"
android:layout_width="wrap_content"
android:background="@drawable/bg_color_btn_mask"
android:layout_gravity="top|right" />
<LinearLayout
android:id="@+id/note_bg_color_selector"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:background="@android:drawable/dialog_holo_light_frame"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginRight="8dp"
android:layout_height="wrap_content"
android:background="@drawable/note_edit_color_selector_panel"
android:layout_marginTop="30dip"
android:layout_marginRight="8dip"
android:layout_gravity="top|right"
android:orientation="horizontal"
android:weightSum="6"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:gravity="center_vertical"
android:visibility="gone">
<!-- 1. 黄色 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_yellow"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/edit_yellow" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_yellow_select"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginBottom="8dp"
android:layout_marginRight="4dp"
android:layout_marginRight="5dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<!-- 2. 蓝色 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_blue"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/edit_blue" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_blue_select"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginBottom="8dp"
android:layout_marginRight="4dp"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dip"
android:src="@drawable/selected" />
</FrameLayout>
<!-- 3. 白色 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_white"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/edit_white" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_white_select"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginBottom="8dp"
android:layout_marginRight="4dp"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dip"
android:src="@drawable/selected" />
</FrameLayout>
<!-- 4. 绿色 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_green"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/edit_green" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_green_select"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginBottom="8dp"
android:layout_marginRight="4dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<!-- 5. 红色 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_red"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/edit_red" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_red_select"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginBottom="8dp"
android:layout_marginRight="4dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<!-- 6. 自定义图片按钮 -->
<FrameLayout
android:layout_width="48dp"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_bg_custom"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center"
android:src="@android:drawable/ic_menu_gallery"
android:padding="6dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:scaleType="fitCenter" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/font_size_selector"
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/font_size_selector_bg"
android:layout_gravity="bottom"
@ -389,33 +247,37 @@
<FrameLayout
android:id="@+id/ll_font_small"
android:layout_width="0dp"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_small"
android:layout_marginBottom="5dp" />
android:layout_marginBottom="5dip" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_small"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_small_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
@ -423,59 +285,68 @@
<FrameLayout
android:id="@+id/ll_font_normal"
android:layout_width="0dp"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_normal"
android:layout_marginBottom="5dp" />
android:layout_marginBottom="5dip" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_normal"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_medium_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:src="@drawable/selected"
android:visibility="gone" />
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_large"
android:layout_width="0dp"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_large"
android:layout_marginBottom="5dp" />
android:layout_marginBottom="5dip" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_large"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_large_select"
android:layout_width="wrap_content"
@ -483,33 +354,37 @@
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:id="@+id/ll_font_super"
android:layout_width="0dp"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="center"
android:gravity="center">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/font_super"
android:layout_marginBottom="5dp" />
android:layout_marginBottom="5dip" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/menu_font_super"
android:textAppearance="@style/TextAppearanceUnderMenuIcon" />
</LinearLayout>
<ImageView
android:id="@+id/iv_super_select"
android:layout_width="wrap_content"
@ -517,24 +392,9 @@
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="6dp"
android:layout_marginBottom="-7dp"
android:layout_marginRight="6dip"
android:layout_marginBottom="-7dip"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<!-- [新增] 字数统计悬浮球 -->
<TextView
android:id="@+id/tv_char_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="16dp"
android:layout_marginBottom="74dp"
android:background="@drawable/bg_char_count"
android:textColor="@android:color/white"
android:textSize="12sp"
android:text="0"
android:visibility="visible" />
</FrameLayout>
</FrameLayout>

@ -15,71 +15,64 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/note_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- 1. 标题栏:占一行,多出部分加省略号 -->
<TextView
android:id="@+id/tv_note_title"
android:layout_width="match_parent"
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:ellipsize="end"
android:textStyle="bold"
android:textColor="@color/primary_text_black"
android:textSize="16sp"
android:layout_marginBottom="6dp" />
android:layout_gravity="center_vertical"
android:gravity="center_vertical">
<!-- 2. 预览内容:限高逻辑 -->
<!-- maxHeight 设为 120dp结合 wrap_content 实现:不到上限全显示,超上限截断 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="120dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2"
android:textColor="#666666"
android:textSize="14sp" />
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<!-- 3. 时间底部栏 -->
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="#999999" />
<CheckBox
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:visibility="gone" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="0dip"
android:layout_weight="1"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:visibility="gone" />
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:visibility="gone"/>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
</LinearLayout>
</LinearLayout>
<CheckBox
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:visibility="gone" />
</LinearLayout>
<!-- [新增] 置顶图标 -->
<!-- 使用系统自带的 star_on 图标代替置顶图钉,位置同样靠右 -->
<ImageView
android:id="@+id/iv_pin_icon"
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:drawable/star_on"
android:layout_gravity="top|right"
android:layout_marginTop="-20dp"
android:contentDescription="Pinned"
android:visibility="gone"/>
</LinearLayout>
android:layout_gravity="top|right"/>
</FrameLayout>

@ -1,117 +1,58 @@
<?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.
-->
<FrameLayout
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:fitsSystemWindows="true"
android:id="@+id/note_list_root"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 1. 顶部 Toolbar -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/main_background_gray"
app:titleTextColor="@color/primary_text_black"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:layout_alignParentTop="true" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<!-- 2. 标题栏 (搜索结果/私密模式标题) -->
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bar_bg"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="#FFEAD1AE"
android:textSize="@dimen/text_font_size_medium"
android:layout_below="@id/toolbar" />
<!-- 3. 顶部文件夹导航栏 -->
<HorizontalScrollView
android:id="@+id/folder_nav_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/main_background_gray"
android:scrollbars="none"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:layout_below="@id/tv_title_bar">
<LinearLayout
android:id="@+id/folder_nav_container"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:orientation="horizontal"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:gravity="center_vertical" />
</HorizontalScrollView>
<!-- 4. [核心新增] 底部导航栏 -->
<!-- 背景设为灰色,图标和文字设为黑色 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_alignParentBottom="true"
android:background="#F5F5F5"
app:itemIconTint="@android:color/black"
app:itemTextColor="@android:color/black"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
<!-- 5. 下拉刷新与列表容器 -->
<!-- 手术关键点:除了 layout_below必须增加 layout_above="@id/bottom_navigation" -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/folder_nav_scroll"
android:layout_above="@id/bottom_navigation">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notes_list_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 6. 用于承载“日程”和“AI”界面的占位容器 (默认隐藏) -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:layout_above="@id/bottom_navigation"
android:visibility="gone" />
</RelativeLayout>
<!-- 7. 底部悬浮按钮 (FAB) -->
<!-- 调整 margin_bottom 以免挡住底部导航栏 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_note"
android:layout_width="wrap_content"
android:textSize="@dimen/text_font_size_medium" />
<ListView
android:id="@+id/notes_list"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
</LinearLayout>
<Button
android:id="@+id/btn_new_note"
android:background="@drawable/new_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|left"
android:layout_marginLeft="24dp"
android:layout_marginBottom="80dp"
android:contentDescription="@string/notelist_menu_new"
android:src="@android:drawable/ic_input_add"
app:backgroundTint="@color/mi_gold"
app:fabSize="normal"
app:rippleColor="@color/pure_white"
app:elevation="6dp" />
</FrameLayout>
android:focusable="false"
android:layout_gravity="bottom" />
</FrameLayout>

@ -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_notes"
android:icon="@android:drawable/ic_menu_edit"
android:title="便签" />
<item
android:id="@+id/nav_agenda"
android:icon="@android:drawable/ic_menu_today"
android:title="日程" />
<item
android:id="@+id/nav_ai"
android:icon="@android:drawable/ic_menu_view"
android:title="AI助手" />
</menu>

@ -18,10 +18,22 @@
<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_delete"
android:title="@string/menu_delete"/>
<item
android:id="@+id/menu_font_size"
android:title="@string/menu_font_size"/>
<item
android:id="@+id/menu_list_mode"
android:title="@string/menu_list_mode" />
<item
android:id="@+id/menu_share"
android:title="@string/menu_share"/>
@ -37,10 +49,4 @@
<item
android:id="@+id/menu_delete_remind"
android:title="@string/menu_remove_remind" />
<item
android:id="@+id/menu_add_to_agenda_ai"
android:title="添加到日程"
android:icon="@android:drawable/ic_menu_my_calendar"
android:showAsAction="never" />
</menu>

@ -25,17 +25,9 @@
android:id="@+id/menu_export_text"
android:title="@string/menu_export_text"/>
<!-- [修改] 原 menu_sync 改为 menu_pull -->
<item
android:id="@+id/menu_pull"
android:title="@string/menu_pull"
android:icon="@android:drawable/ic_popup_sync" />
<!-- [新增] 新增 menu_push -->
<item
android:id="@+id/menu_push"
android:title="@string/menu_push"
android:icon="@android:drawable/ic_menu_upload" />
android:id="@+id/menu_sync"
android:title="@string/menu_sync"/>
<item
android:id="@+id/menu_setting"
@ -44,15 +36,4 @@
<item
android:id="@+id/menu_search"
android:title="@string/menu_search"/>
<item
android:id="@+id/menu_recycle_bin"
android:title="@string/menu_recycle_bin"
android:icon="@android:drawable/ic_menu_delete"
android:showAsAction="never" />
<item
android:id="@+id/menu_account_logout"
android:title="注销登录"
android:showAsAction="never" />
</menu>

@ -23,41 +23,9 @@
android:icon="@drawable/menu_move"
android:showAsAction="always|withText" />
<item
android:id="@+id/menu_pin"
android:title="@string/menu_pin"
android:icon="@android:drawable/ic_menu_upload"
android:showAsAction="ifRoom|withText" />
<!-- 新增:取消置顶 (默认隐藏,代码逻辑控制显示) -->
<item
android:id="@+id/menu_unpin"
android:title="@string/menu_unpin"
android:icon="@android:drawable/ic_menu_upload"
android:visible="false"
android:showAsAction="ifRoom|withText" />
<!-- 新增:设为私密 -->
<item
android:id="@+id/menu_private"
android:title="@string/menu_private"
android:icon="@android:drawable/ic_menu_view"
android:showAsAction="ifRoom|withText" />
<item
android:id="@+id/delete"
android:title="@string/menu_delete"
android:icon="@drawable/menu_delete"
android:showAsAction="always|withText" />
<item
android:id="@+id/menu_rename"
android:title="Rename"
android:showAsAction="never" />
<item
android:id="@+id/menu_restore"
android:title="@string/menu_restore"
android:icon="@android:drawable/ic_menu_revert"
android:visible="false"
android:showAsAction="ifRoom|withText" />
</menu>

@ -28,14 +28,4 @@
<item>Messaging</item>
<item>Email</item>
</string-array>
<string-array name="view_mode_entries">
<item>List</item>
<item>Waterfall</item>
</string-array>
<string-array name="view_mode_values">
<item>0</item>
<item>1</item>
</string-array>
</resources>

@ -17,15 +17,4 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<!-- 为 UI 改造新增的颜色资源 -->
<color name="main_background_gray">#F5F5F5</color>
<color name="main_background_dark_gray">#E0E0E0</color>
<color name="mi_gold">#FFD700</color>
<color name="pure_white">#FFFFFF</color>
<color name="primary_text_black">#333333</color>
<color name="nav_background">#F5F5F5</color> <!-- 浅灰色背景 -->
<color name="nav_item_selected">#000000</color> <!-- 选中时为黑色 -->
<color name="nav_item_unselected">#757575</color> <!-- 未选中时为中灰色 -->
</resources>

@ -1,4 +1,5 @@
<?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");
@ -57,46 +58,12 @@
<item name="android:textColor">@color/secondary_text_dark</item>
</style>
<style name="NoteTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="colorPrimary">@color/main_background_gray</item>
<item name="colorPrimaryDark">@color/main_background_dark_gray</item>
<item name="colorAccent">@color/mi_gold</item>
<item name="android:textColorPrimary">@color/primary_text_black</item>
<!-- <item name="actionBarStyle">@style/NoteActionBarStyle</item> -->
<style name="NoteTheme" parent="@android:style/Theme.Holo.Light">
<item name="android:actionBarStyle">@style/NoteActionBarStyle</item>
</style>
<style name="NoteActionBarStyle" parent="Widget.AppCompat.ActionBar.Solid">
<item name="background">@color/main_background_gray</item>
<item name="displayOptions">showTitle|useLogo|showHome</item>
<item name="titleTextStyle">@style/NoteActionBarTitle</item>
</style>
<style name="NoteActionBarTitle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
<item name="android:textColor">#000000</item>
<style name="NoteActionBarStyle" parent="@android:style/Widget.Holo.Light.ActionBar.Solid">
<item name="android:displayOptions" />
<item name="android:visibility">gone</item>
</style>
<string name="menu_pin">Pin</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_private">Private</string>
<string name="alert_move_to_private">Move selected notes to private folder?</string>
<string name="title_private_folder">Private Folder</string>
<string name="input_password">Input Password</string>
<string name="password_error">Wrong password!</string>
<string name="title_recycle_bin">Recycle Bin</string>
<string name="menu_recycle_bin">Recycle Bin</string>
<string name="menu_restore">Restore</string>
<string name="alert_delete_forever">Delete these notes forever?</string>
<string name="char_count_format">%1$d 字</string>
<string name="menu_push">Push</string>
<string name="menu_pull">Pull</string>
<string name="preferences_floating_window_title">全局悬浮球</string>
<string name="preferences_floating_window_summary">开启后可在屏幕边缘显示快捷图标,点击立即记录灵感</string>
<string name="toast_need_overlay_permission">开启悬浮球需要“显示在其他应用上”的权限,请手动授予</string>
</resources>
</resources>

@ -26,28 +26,5 @@
android:key="pref_key_bg_random_appear"
android:title="@string/preferences_bg_random_appear_title"
android:defaultValue="false" />
<ListPreference
android:key="pref_view_mode"
android:title="Switch View"
android:entries="@array/view_mode_entries"
android:entryValues="@array/view_mode_values"
android:defaultValue="0" />
<CheckBoxPreference
android:key="pref_key_enable_floating"
android:title="@string/preferences_floating_window_title"
android:summary="@string/preferences_floating_window_summary"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="个性化">
<Preference
android:key="pref_key_custom_list_bg"
android:title="更换列表背景图片"
android:summary="选择一张图片作为主列表背景" />
</PreferenceCategory>
<Preference
android:key="pref_key_reset_list_bg"
android:title="恢复默认列表背景"
android:summary="移除自定义图片,还原系统默认风格" />
</PreferenceScreen>

@ -20,7 +20,6 @@
android:label="@string/search_label"
android:hint="@string/search_hint"
android:searchMode="queryRewriteFromText"
android:imeOptions="actionSearch"
android:searchSuggestAuthority="notes"
android:searchSuggestIntentAction="android.intent.action.VIEW"

@ -25,19 +25,10 @@ import android.util.Log;
import java.util.HashMap;
/**
*
* Contact
*/
public class Contact {
/** 联系人缓存,用于提高查询效率 */
private static HashMap<String, String> sContactCache;
/** 日志标签 */
private static final String TAG = "Contact";
/**
*
*/
private static final String CALLER_ID_SELECTION = "PHONE_NUMBERS_EQUAL(" + Phone.NUMBER
+ ",?) AND " + Data.MIMETYPE + "='" + Phone.CONTENT_ITEM_TYPE + "'"
+ " AND " + Data.RAW_CONTACT_ID + " IN "
@ -45,49 +36,36 @@ public class Contact {
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
/**
*
* @param context
* @param phoneNumber
* @return null
*/
public static String getContact(Context context, String phoneNumber) {
// 初始化缓存
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
}
// 检查缓存中是否已有该电话号码对应的联系人
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
// 构建查询条件替换min_match参数
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
// 查询联系人数据库
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME }, // 只查询联系人姓名
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber }, // 绑定电话号码参数
new String[] { phoneNumber },
null);
// 处理查询结果
if (cursor != null && cursor.moveToFirst()) {
try {
// 获取联系人姓名
String name = cursor.getString(0);
// 将结果存入缓存
sContactCache.put(phoneNumber, name);
return name;
} catch (IndexOutOfBoundsException e) {
// 处理异常
Log.e(TAG, " Cursor get string error " + e.toString());
return null;
} finally {
// 关闭游标
cursor.close();
}
} else {
// 未找到匹配的联系人
Log.d(TAG, "No contact matched with number:" + phoneNumber);
return null;
}

@ -17,441 +17,263 @@
package net.micode.notes.data;
import android.net.Uri;
/**
* Notes 使
*/
public class Notes {
/** 内容提供者的权限 */
public static final String AUTHORITY = "micode_notes";
/** 日志标签 */
public static final String TAG = "Notes";
/** 便签类型 */
public static final int TYPE_NOTE = 0;
/** 文件夹类型 */
public static final int TYPE_FOLDER = 1;
/** 系统类型 */
public static final int TYPE_SYSTEM = 2;
/**
*
* {@link Notes#ID_ROOT_FOLDER }
* {@link Notes#ID_TEMPARAY_FOLDER } 便
* {@link Notes#ID_CALL_RECORD_FOLDER}
* Following IDs are system folders' identifiers
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
*/
public static final int ID_ROOT_FOLDER = 0;
/** 临时文件夹,用于移动便签时 */
public static final int ID_TEMPARAY_FOLDER = -1;
/** 通话记录文件夹 */
public static final int ID_CALL_RECORD_FOLDER = -2;
/** 回收站文件夹 */
public static final int ID_TRASH_FOLER = -3;
/** 意图额外信息:提醒日期 */
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
/** 意图额外信息背景颜色ID */
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
/** 意图额外信息小部件ID */
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
/** 意图额外信息:小部件类型 */
public static final String INTENT_EXTRA_WIDGET_TYPE = "net.micode.notes.widget_type";
/** 意图额外信息文件夹ID */
public static final String INTENT_EXTRA_FOLDER_ID = "net.micode.notes.folder_id";
/** 意图额外信息:通话日期 */
public static final String INTENT_EXTRA_CALL_DATE = "net.micode.notes.call_date";
/** 无效的小部件类型 */
public static final int TYPE_WIDGET_INVALIDE = -1;
/** 2x 小部件类型 */
public static final int TYPE_WIDGET_2X = 0;
/** 4x 小部件类型 */
public static final int TYPE_WIDGET_4X = 1;
/**
* MIME
*/
public static class DataConstants {
/** 文本便签的内容项类型 */
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
/** 通话便签的内容项类型 */
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
/**
* 便 Uri
* Uri to query all notes and folders
*/
public static final Uri CONTENT_NOTE_URI = Uri.parse("content://" + AUTHORITY + "/note");
/**
* Uri
* Uri to query data
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
/**
* 便
*/
public interface NoteColumns {
/**
* ID
* <P> : INTEGER (long) </P>
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* 便ID
* <P> : INTEGER (long) </P>
* The parent's id for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String PARENT_ID = "parent_id";
/**
* 便
* <P> : INTEGER (long) </P>
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
*
* <P> : INTEGER (long) </P>
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : INTEGER (long) </P>
* Alert date
* <P> Type: INTEGER (long) </P>
*/
public static final String ALERTED_DATE = "alert_date";
/**
* 便
* <P> : TEXT </P>
* Folder's name or text content of note
* <P> Type: TEXT </P>
*/
public static final String SNIPPET = "snippet";
/**
* 便
* : TEXT
*/
public static final String TITLE = "title";
/**
* 便ID
* <P> : INTEGER (long) </P>
* Note's widget id
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_ID = "widget_id";
/**
* 便
* <P> : INTEGER (long) </P>
* Note's widget type
* <P> Type: INTEGER (long) </P>
*/
public static final String WIDGET_TYPE = "widget_type";
/**
* 便ID
* <P> : INTEGER (long) </P>
* Note's background color's id
* <P> Type: INTEGER (long) </P>
*/
public static final String BG_COLOR_ID = "bg_color_id";
/**
* 便便
* <P> : INTEGER </P>
* For text note, it doesn't has attachment, for multi-media
* note, it has at least one attachment
* <P> Type: INTEGER </P>
*/
public static final String HAS_ATTACHMENT = "has_attachment";
/**
* 便
* <P> : INTEGER (long) </P>
* Folder's count of notes
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTES_COUNT = "notes_count";
/**
* 便
* <P> : INTEGER </P>
* The file type: folder or note
* <P> Type: INTEGER </P>
*/
public static final String TYPE = "type";
/**
* ID
* <P> : INTEGER (long) </P>
* The last sync id
* <P> Type: INTEGER (long) </P>
*/
public static final String SYNC_ID = "sync_id";
/**
*
* <P> : INTEGER </P>
* Sign to indicate local modified or not
* <P> Type: INTEGER </P>
*/
public static final String LOCAL_MODIFIED = "local_modified";
/**
* ID
* <P> : INTEGER </P>
* Original parent id before moving into temporary folder
* <P> Type : INTEGER </P>
*/
public static final String ORIGIN_PARENT_ID = "origin_parent_id";
/**
* GTask ID
* <P> : TEXT </P>
* The gtask id
* <P> Type : TEXT </P>
*/
public static final String GTASK_ID = "gtask_id";
/**
*
* <P> : INTEGER (long) </P>
* The version code
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
/**
* (0-, 1-)
* : INTEGER
*/
public static final String IS_PINNED = "is_pinned";
/**
* (0-, 1-)
* : INTEGER
*/
public static final String IS_PRIVATE = "is_private";
/**
* (0-, 1-)
* : INTEGER
*/
public static final String VIEW_MODE = "view_mode";
/**
* URI
* : TEXT
*/
public static final String CUSTOM_BG_URI = "custom_bg_uri";
/**
* 1 ()
* : TEXT
*/
public static final String EXPAND_1 = "expand_1";
/**
* (0:, 1:/, 2:)
* : INTEGER
*/
public static final String SYNC_STATE = "sync_state";
/**
* ID (Firebase Document ID)
* : TEXT
*/
public static final String SERVER_ID = "server_id";
/**
* (0:便, 1:)
* : INTEGER
*/
public static final String IS_AGENDA = "is_agenda";
/**
* ()
* : INTEGER
*/
public static final String AGENDA_DATE = "agenda_date";
/**
* (0:, 1:)
* : INTEGER
*/
public static final String IS_COMPLETED = "is_completed";
/**
* AI (Happy, Sad, Urgent...)
* : TEXT
*/
public static final String EMOTION_TAG = "emotion_tag";
/**
*
*/
public static final String AGENDA_END_DATE = "agenda_end_date";
/**
*
*/
public static final String TIME_LABEL = "time_label";
}
/**
*
*/
public interface AccountColumns {
/** Firebase UID */
public static final String UID = "uid";
/** 邮箱地址 */
public static final String EMAIL = "email";
/** 同步令牌 */
public static final String TOKEN = "token";
/** 头像URL */
public static final String AVATAR_URL = "avatar_url";
}
/**
*
*/
public interface ChatColumns {
/** 消息ID */
public static final String ID = "_id";
/**
* :
* 0: (User)
* 1: AI (Assistant)
* 2: /Prompt (System - UI)
*/
public static final String SENDER_TYPE = "sender_type";
/**
* :
* 0: (Text)
* 1: (Reminder Card)
*/
public static final String MSG_TYPE = "msg_type";
/** 普通对话消息类型 */
public static final int MSG_TYPE_TEXT = 0;
/** 提醒卡片消息类型 */
public static final int MSG_TYPE_REMINDER = 1;
/** 消息内容 */
public static final String CONTENT = "content";
/** 消息创建时间 */
public static final String CREATED_AT = "created_at";
}
/**
*
*/
public interface DataColumns {
/**
* ID
* <P> : INTEGER (long) </P>
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
*/
public static final String ID = "_id";
/**
* MIME
* <P> : Text </P>
* The MIME type of the item represented by this row.
* <P> Type: Text </P>
*/
public static final String MIME_TYPE = "mime_type";
/**
* 便ID
* <P> : INTEGER (long) </P>
* The reference id to note that this data belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String NOTE_ID = "note_id";
/**
* 便
* <P> : INTEGER (long) </P>
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
public static final String CREATED_DATE = "created_date";
/**
*
* <P> : INTEGER (long) </P>
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
public static final String MODIFIED_DATE = "modified_date";
/**
*
* <P> : TEXT </P>
* Data's content
* <P> Type: TEXT </P>
*/
public static final String CONTENT = "content";
/**
* {@link #MIME_TYPE}
* <P> : INTEGER </P>
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA1 = "data1";
/**
* {@link #MIME_TYPE}
* <P> : INTEGER </P>
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* integer data type
* <P> Type: INTEGER </P>
*/
public static final String DATA2 = "data2";
/**
* {@link #MIME_TYPE}
* <P> : TEXT </P>
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA3 = "data3";
/**
* {@link #MIME_TYPE}
* <P> : TEXT </P>
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA4 = "data4";
/**
* {@link #MIME_TYPE}
* <P> : TEXT </P>
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
* TEXT data type
* <P> Type: TEXT </P>
*/
public static final String DATA5 = "data5";
}
/**
* 便DataColumns
*/
public static final class TextNote implements DataColumns {
/**
*
* <P> : Integer 1: 0: </P>
* Mode to indicate the text in check list mode or not
* <P> Type: Integer 1:check list mode 0: normal mode </P>
*/
public static final String MODE = DATA1;
/** 检查列表模式 */
public static final int MODE_CHECK_LIST = 1;
/** 文本便签的内容类型 */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";
/** 文本便签的内容项类型 */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
/** 文本便签的内容URI */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
/**
* 便DataColumns
*/
public static final class CallNote implements DataColumns {
/**
*
* <P> : INTEGER (long) </P>
* Call date for this record
* <P> Type: INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;
/**
*
* <P> : TEXT </P>
* Phone number for this record
* <P> Type: TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;
/** 通话便签的内容类型 */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";
/** 通话便签的内容项类型 */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
/** 通话便签的内容URI */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
/**
* 便DataColumns
*/
public static final class ImageNote implements DataColumns {
/** 借用CONTENT字段存储图片的URI字符串 */
public static final String IMAGE_URI = CONTENT;
/** 图片便签的内容类型 */
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image_note";
/** 图片便签的内容项类型 */
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/image_note";
/** 图片便签的内容URI */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/image_note");
}
}

@ -27,35 +27,19 @@ import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* NotesDatabaseHelper
*
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
/** 数据库名称 */
private static final String DB_NAME = "note.db";
/** 数据库版本 */
private static final int DB_VERSION = 11;
private static final int DB_VERSION = 4;
/**
*
*/
public interface TABLE {
/** 便签表 */
public static final String NOTE = "note";
/** 数据表 */
public static final String DATA = "data";
/** 用户账号表 */
public static final String USER_ACCOUNT = "user_account";
/** 聊天消息表 */
public static final String CHAT_MESSAGES = "chat_messages";
}
/** 日志标签 */
private static final String TAG = "NotesDatabaseHelper";
/** 单例实例 */
private static NotesDatabaseHelper mInstance;
private static final String CREATE_NOTE_TABLE_SQL =
@ -76,43 +60,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.TITLE + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.IS_PINNED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.IS_PRIVATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.VIEW_MODE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CUSTOM_BG_URI + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.EXPAND_1 + " TEXT NOT NULL DEFAULT ''," +
// [新增 v9]
NoteColumns.SYNC_STATE + " INTEGER DEFAULT 1," +
NoteColumns.SERVER_ID + " TEXT," +
NoteColumns.IS_AGENDA + " INTEGER DEFAULT 0," +
NoteColumns.AGENDA_DATE + " INTEGER DEFAULT 0," +
NoteColumns.IS_COMPLETED + " INTEGER DEFAULT 0," +
NoteColumns.EMOTION_TAG + " TEXT," +
NoteColumns.AGENDA_END_DATE + " INTEGER DEFAULT 0," +
NoteColumns.TIME_LABEL + " TEXT DEFAULT ''" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
// [新增] 创建用户账号表SQL
private static final String CREATE_USER_TABLE_SQL =
"CREATE TABLE " + TABLE.USER_ACCOUNT + "(" +
Notes.AccountColumns.UID + " TEXT PRIMARY KEY," +
Notes.AccountColumns.EMAIL + " TEXT," +
Notes.AccountColumns.TOKEN + " TEXT," +
Notes.AccountColumns.AVATAR_URL + " TEXT" +
")";
// [新增] 创建聊天消息表SQL
private static final String CREATE_CHAT_TABLE_SQL =
"CREATE TABLE " + TABLE.CHAT_MESSAGES + "(" +
Notes.ChatColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Notes.ChatColumns.SENDER_TYPE + " INTEGER DEFAULT 0," +
Notes.ChatColumns.MSG_TYPE + " INTEGER DEFAULT 0," +
Notes.ChatColumns.CONTENT + " TEXT," +
Notes.ChatColumns.CREATED_AT + " INTEGER" +
")";
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
@ -256,18 +206,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
*
* @param context
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* 便
* @param db SQLite
*/
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
@ -275,10 +217,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "note table has been created");
}
/**
* 便
* @param db SQLite
*/
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");
@ -297,22 +235,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/**
*
* @param db SQLite
*/
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);
@ -320,7 +254,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
/**
*
* temporary folder which is used for moving note
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
@ -328,7 +262,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
/**
*
* create trash folder
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
@ -336,10 +270,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
}
/**
*
* @param db SQLite
*/
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
@ -347,10 +277,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "data table has been created");
}
/**
*
* @param db SQLite
*/
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");
@ -361,11 +287,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
/**
*
* @param context
* @return NotesDatabaseHelper
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
@ -374,33 +295,19 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
}
@Override
/**
*
* @param db SQLite
*/
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
// 创建用户账号表
db.execSQL(CREATE_USER_TABLE_SQL);
// 创建聊天消息表
db.execSQL(CREATE_CHAT_TABLE_SQL);
}
@Override
/**
*
* @param db SQLite
* @param oldVersion
* @param newVersion
*/
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
boolean skipV2 = false;
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // Skip v2 upgrade since it's already done
skipV2 = true; // this upgrade including the upgrade from v2 to v3
oldVersion++;
}
@ -412,40 +319,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
if (oldVersion == 3) {
upgradeToV4(db);
reCreateTriggers = true;
oldVersion++;
}
if (oldVersion == 4) {
upgradeToV5(db);
reCreateTriggers = true;
oldVersion++;
}
if (oldVersion == 5) {
upgradeToV6(db);
reCreateTriggers = true;
oldVersion++;
}
// [??] ???????? v6 ???? v9???????
if (oldVersion < 9) {
upgradeToV9(db);
oldVersion = 9;
}
// [???] ?? v10 ???
if (oldVersion < 10) {
upgradeToV10(db);
oldVersion = 10;
}
// [??] ??? v11
if (oldVersion < 11) {
upgradeToV11(db);
oldVersion = 11;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@ -457,39 +333,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
}
}
private void upgradeToV11(SQLiteDatabase db) {
Log.d(TAG, "Upgrading database to version 11...");
// ??????????????????????????
db.execSQL("DROP TABLE IF EXISTS " + TABLE.CHAT_MESSAGES);
db.execSQL(CREATE_CHAT_TABLE_SQL);
}
private void upgradeToV10(SQLiteDatabase db) {
Log.d(TAG, "Upgrading database to version 10...");
// ?? try-catch ????????????????????????
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.AGENDA_END_DATE + " INTEGER DEFAULT 0");
} catch (Exception e) {
Log.w(TAG, "Column agenda_end_date already exists");
}
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TIME_LABEL + " TEXT DEFAULT ''");
} catch (Exception e) {
Log.w(TAG, "Column time_label already exists");
}
}
// ???? V6 ???????????
private void upgradeToV6(SQLiteDatabase db) {
// ?? try-catch ??"????"????????????
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_PINNED + " INTEGER NOT NULL DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_PRIVATE + " INTEGER NOT NULL DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VIEW_MODE + " INTEGER NOT NULL DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.CUSTOM_BG_URI + " TEXT NOT NULL DEFAULT ''"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.EXPAND_1 + " TEXT NOT NULL DEFAULT ''"); } catch(Exception e){}
}
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@ -516,31 +359,4 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_PINNED + " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_PRIVATE + " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VIEW_MODE + " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.CUSTOM_BG_URI + " TEXT NOT NULL DEFAULT ''");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.EXPAND_1 + " TEXT NOT NULL DEFAULT ''");
}
private void upgradeToV9(SQLiteDatabase db) {
Log.d(TAG, "Upgrading database to version 9...");
// 1. 为 NOTE 表添加字段
// 使用 try-catch 防止 ALTER TABLE 语句执行失败
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.SYNC_STATE + " INTEGER DEFAULT 1"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.SERVER_ID + " TEXT"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_AGENDA + " INTEGER DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.AGENDA_DATE + " INTEGER DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_COMPLETED + " INTEGER DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.EMOTION_TAG + " TEXT"); } catch(Exception e){}
// 2. 创建新表
try { db.execSQL(CREATE_USER_TABLE_SQL); } catch(Exception e){ Log.e(TAG, "Create user table failed: " + e); }
try { db.execSQL(CREATE_CHAT_TABLE_SQL); } catch(Exception e){ Log.e(TAG, "Create chat table failed: " + e); }
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN agenda_end_date INTEGER DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN time_label TEXT DEFAULT ''"); } catch(Exception e){}
}
}

@ -35,50 +35,30 @@ import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
/**
* NotesProvider 便
*
*/
public class NotesProvider extends ContentProvider {
/** URI匹配器用于解析Content URI */
private static final UriMatcher mMatcher;
/** 数据库助手类实例 */
private NotesDatabaseHelper mHelper;
/** 日志标签 */
private static final String TAG = "NotesProvider";
/** URI类型便签集合 */
private static final int URI_NOTE = 1;
/** URI类型单个便签 */
private static final int URI_NOTE_ITEM = 2;
/** URI类型数据集合 */
private static final int URI_DATA = 3;
/** URI类型单个数据 */
private static final int URI_DATA_ITEM = 4;
/** URI类型搜索 */
private static final int URI_SEARCH = 5;
/** URI类型搜索建议 */
private static final int URI_SEARCH_SUGGEST = 6;
/** URI类型用户账号 */
private static final int URI_USER_ACCOUNT = 7;
/** URI类型聊天消息 */
private static final int URI_CHAT_MESSAGES = 8;
static {
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
mMatcher.addURI(Notes.AUTHORITY, "note/*", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, "user_account", URI_USER_ACCOUNT);
// [新增] 注册聊天消息URI content://micode_notes/chat_messages
mMatcher.addURI(Notes.AUTHORITY, "chat_messages", URI_CHAT_MESSAGES);
}
/**
@ -100,25 +80,12 @@ public class NotesProvider extends ContentProvider {
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
@Override
/**
*
* @return
*/
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
@Override
/**
*
* @param uri URI
* @param projection
* @param selection
* @param selectionArgs
* @param sortOrder
* @return
*/
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
@ -171,9 +138,6 @@ public class NotesProvider extends ContentProvider {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
case URI_CHAT_MESSAGES:
c = db.query(NotesDatabaseHelper.TABLE.CHAT_MESSAGES, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -184,12 +148,6 @@ public class NotesProvider extends ContentProvider {
}
@Override
/**
*
* @param uri URI
* @param values
* @return URI
*/
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
@ -205,15 +163,6 @@ public class NotesProvider extends ContentProvider {
}
insertedId = dataId = db.insert(TABLE.DATA, null, values);
break;
case URI_USER_ACCOUNT:
insertedId = db.insert(TABLE.USER_ACCOUNT, null, values);
break;
// [新增] 处理聊天消息插入
case URI_CHAT_MESSAGES:
insertedId = db.insert(NotesDatabaseHelper.TABLE.CHAT_MESSAGES, null, values);
// 通知内容解析器,以便 ChatFragment 可以更新
getContext().getContentResolver().notifyChange(uri, null);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
@ -233,13 +182,6 @@ public class NotesProvider extends ContentProvider {
}
@Override
/**
*
* @param uri URI
* @param selection
* @param selectionArgs
* @return
*/
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
@ -286,42 +228,18 @@ public class NotesProvider extends ContentProvider {
}
@Override
/**
*
* @param uri URI
* @param values
* @param selection
* @param selectionArgs
* @return
*/
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 跳过同步标签的标志
boolean skipSyncTag = false;
int match = mMatcher.match(uri);
switch (match) {
switch (mMatcher.match(uri)) {
case URI_NOTE:
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
// 检查ID是否为系统文件夹
try {
long numericId = Long.parseLong(id);
if (numericId <= 0) {
skipSyncTag = true; // ID为-3, -2, -1, 0时跳过同步标签
}
} catch (NumberFormatException e) {
skipSyncTag = true; // 非数字ID时跳过同步标签
}
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
@ -329,34 +247,17 @@ public class NotesProvider extends ContentProvider {
case URI_DATA:
count = db.update(TABLE.DATA, values, selection, selectionArgs);
updateData = true;
// 由于DATA表的更新会自动触发Note表的更新
// 因此跳过同步标签以避免重复更新
skipSyncTag = true;
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
count = db.update(TABLE.DATA, values, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
updateData = true;
skipSyncTag = true;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 处理同步标签
// 如果跳过同步标签为false且values中不包含SYNC_STATE则设置为1待同步
if (!skipSyncTag) {
if (!values.containsKey(NoteColumns.SYNC_STATE)) {
values.put(NoteColumns.SYNC_STATE, 1);
// 由于db.update已执行
// 因此需要单独更新Note表的SYNC_STATE字段
if (id != null && match == URI_NOTE_ITEM) {
db.execSQL("UPDATE " + TABLE.NOTE + " SET " + NoteColumns.SYNC_STATE + "=1 WHERE " + NoteColumns.ID + "=" + id);
}
}
}
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@ -366,21 +267,10 @@ public class NotesProvider extends ContentProvider {
return count;
}
/**
*
* @param selection
* @return
*/
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
/**
* 便
* @param id 便ID
* @param selection
* @param selectionArgs
*/
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -407,11 +297,6 @@ public class NotesProvider extends ContentProvider {
}
@Override
/**
*
* @param uri URI
* @return
*/
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,41 +0,0 @@
package net.micode.notes.model;
/**
*
* AI
*/
public class ChatMessage {
/**
* ID
*/
public long id;
/**
* 0-1-AI
*/
public int senderType;
/**
* 0-1-
*/
public int msgType;
/**
*
*/
public String content;
/**
*
*/
public long createdAt;
/**
*
* @param senderType
* @param msgType
* @param content
*/
public ChatMessage(int senderType, int msgType, String content) {
this.senderType = senderType;
this.msgType = msgType;
this.content = content;
this.createdAt = System.currentTimeMillis();
}
}

@ -1,71 +0,0 @@
package net.micode.notes.model;
import com.google.firebase.firestore.IgnoreExtraProperties;
/**
* 便
* Firebase Firestore 便
*/
@IgnoreExtraProperties
public class CloudNote {
/**
* ID
*/
public String serverId;
/**
*
*/
public String title;
/**
*
*/
public String content;
/**
*
*/
public long modifiedDate;
/**
* ID
*/
public int bgColorId;
/**
* 0-1-
*/
public int isAgenda;
/**
*
*/
public long agendaDate;
/**
* 0-1-
*/
public int isCompleted;
/**
* Firebase
*/
public CloudNote() {}
/**
*
* @param serverId ID
* @param title
* @param content
* @param modifiedDate
* @param bgColorId ID
* @param isAgenda (0/1)
* @param agendaDate
* @param isCompleted (0/1)
*/
public CloudNote(String serverId, String title, String content, long modifiedDate,
int bgColorId, int isAgenda, long agendaDate, int isCompleted) {
this.serverId = serverId;
this.title = title;
this.content = content;
this.modifiedDate = modifiedDate;
this.bgColorId = bgColorId;
this.isAgenda = isAgenda;
this.agendaDate = agendaDate;
this.isCompleted = isCompleted;
}
}

@ -34,30 +34,12 @@ import net.micode.notes.data.Notes.TextNote;
import java.util.ArrayList;
/**
* 便
* 便
*/
public class Note {
/**
* 便ContentValues
*/
private ContentValues mNoteDiffValues;
/**
* 便NoteData
*/
private NoteData mNoteData;
/**
*
*/
private static final String TAG = "Note";
/**
* 便便ID
* @param context
* @param folderId ID
* @return 便ID
* @throws IllegalStateException 便
* Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// Create a new note in the database
@ -83,83 +65,41 @@ public class Note {
return noteId;
}
/**
*
* NoteContentValuesNoteData
*/
public Note() {
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
/**
* 便
* @param key
* @param value
*/
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
* 便
* @param key
* @param value
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value);
}
/**
* ID
* @param id ID
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id);
}
/**
* ID
* @return ID
*/
public long getTextDataId() {
return mNoteData.mTextDataId;
}
/**
* ID
* @param id ID
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id);
}
/**
* 便
* @param key
* @param value
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value);
}
/**
* 便
* @return truefalse
*/
public boolean isLocalModified() {
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
/**
* 便
* @param context
* @param noteId 便ID
* @return truefalse
* @throws IllegalArgumentException 便ID
*/
public boolean syncNote(Context context, long noteId) {
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
@ -190,40 +130,17 @@ public class Note {
return true;
}
/**
* 便
* 便
*/
private class NoteData {
/**
* ID
*/
private long mTextDataId;
/**
* ContentValues
*/
private ContentValues mTextDataValues;
/**
* ID
*/
private long mCallDataId;
/**
* ContentValues
*/
private ContentValues mCallDataValues;
/**
*
*/
private static final String TAG = "NoteData";
/**
*
* NoteDataContentValuesID
*/
public NoteData() {
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
@ -231,19 +148,10 @@ public class Note {
mCallDataId = 0;
}
/**
*
* @return truefalse
*/
boolean isLocalModified() {
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
/**
* ID
* @param id ID
* @throws IllegalArgumentException ID0
*/
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
@ -251,11 +159,6 @@ public class Note {
mTextDataId = id;
}
/**
* ID
* @param id ID
* @throws IllegalArgumentException ID0
*/
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
@ -263,35 +166,18 @@ public class Note {
mCallDataId = id;
}
/**
*
* @param key
* @param value
*/
void setCallData(String key, String value) {
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* @param key
* @param value
*/
void setTextData(String key, String value) {
mTextDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* @param context
* @param noteId 便ID
* @return Urinull
* @throws IllegalArgumentException 便ID
*/
Uri pushIntoContentResolver(Context context, long noteId) {
/**
* Check for safety

@ -32,94 +32,36 @@ import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
/**
* 便
* 便
*/
public class WorkingNote {
/**
* 便
*/
// Note for the working note
private Note mNote;
/**
* 便ID
*/
// Note Id
private long mNoteId;
/**
* 便
*/
// Note content
private String mContent;
/**
* 便
*/
// Note mode
private int mMode;
/**
*
*/
private long mAlertDate;
/**
*
*/
private long mModifiedDate;
/**
* ID
*/
private int mBgColorId;
/**
* 便
*/
private String mTitle;
/**
* ID
*/
private int mWidgetId;
/**
*
*/
private int mWidgetType;
/**
* ID
*/
private long mFolderId;
/**
*
*/
private Context mContext;
/**
* URI
*/
private String mCustomBgUri;
/**
*
*/
private static final String TAG = "WorkingNote";
/**
*
*/
private boolean mIsDeleted;
/**
* 便
*/
private NoteSettingChangedListener mNoteSettingStatusListener;
/**
*
*/
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID,
DataColumns.CONTENT,
@ -130,85 +72,36 @@ public class WorkingNote {
DataColumns.DATA4,
};
/**
* 便
*/
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE,
NoteColumns.TITLE,
NoteColumns.CUSTOM_BG_URI
NoteColumns.MODIFIED_DATE
};
/**
* ID
*/
private static final int DATA_ID_COLUMN = 0;
/**
*
*/
private static final int DATA_CONTENT_COLUMN = 1;
/**
* MIME
*/
private static final int DATA_MIME_TYPE_COLUMN = 2;
/**
*
*/
private static final int DATA_MODE_COLUMN = 3;
/**
* 便ID
*/
private static final int NOTE_PARENT_ID_COLUMN = 0;
/**
* 便
*/
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
/**
* 便ID
*/
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
/**
* 便ID
*/
private static final int NOTE_WIDGET_ID_COLUMN = 3;
/**
* 便
*/
private static final int NOTE_WIDGET_TYPE_COLUMN = 4;
/**
* 便
*/
private static final int NOTE_MODIFIED_DATE_COLUMN = 5;
/**
* 便
*/
private static final int NOTE_TITLE_COLUMN = 6;
/**
* 便
*/
private static final int NOTE_CUSTOM_BG_COLUMN = 7;
/**
* 便
* @param context
* @param folderId ID
*/
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0;
@ -221,12 +114,7 @@ public class WorkingNote {
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
/**
* 便
* @param context
* @param noteId 便ID
* @param folderId ID
*/
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId;
@ -236,9 +124,6 @@ public class WorkingNote {
loadNote();
}
/**
* 便
*/
private void loadNote() {
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
@ -252,8 +137,6 @@ public class WorkingNote {
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
mTitle = cursor.getString(NOTE_TITLE_COLUMN);
mCustomBgUri = cursor.getString(NOTE_CUSTOM_BG_COLUMN);
}
cursor.close();
} else {
@ -263,9 +146,6 @@ public class WorkingNote {
loadNoteData();
}
/**
* 便
*/
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
@ -294,15 +174,6 @@ public class WorkingNote {
}
}
/**
* 便
* @param context
* @param folderId ID
* @param widgetId ID
* @param widgetType
* @param defaultBgColorId ID
* @return WorkingNote
*/
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
@ -312,20 +183,10 @@ public class WorkingNote {
return note;
}
/**
* 便
* @param context
* @param id 便ID
* @return WorkingNote
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
* 便
* @return truefalse
*/
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
@ -335,37 +196,11 @@ public class WorkingNote {
}
}
// 1. 只有当标题真的为空时(比如第一次编辑新便签),才执行自动提取逻辑
if (TextUtils.isEmpty(mTitle)) {
String plainText = getPlainText(mContent); // 使用之前提供的清洗工具
if (!TextUtils.isEmpty(plainText)) {
String[] lines = plainText.split("\n");
for (String line : lines) {
if (!TextUtils.isEmpty(line.trim())) {
mTitle = line.trim();
break;
}
}
// 限制长度
if (mTitle != null && mTitle.length() > 20) {
mTitle = mTitle.substring(0, 20);
}
}
}
// 2. 无论标题是刚才生成的,还是以前加载的,都写入数据库更新
// 如果用户在外部进行了“重命名”数据库里的值会变loadNote 会拿到新值,此处也会保留新值
mNote.setNoteValue(NoteColumns.TITLE, mTitle);
// 3. 摘要预览清洗Snippet 依然随正文更新,保证预览的时效性)
String plainContent = getPlainText(mContent);
String snippet = plainContent.replace("\n", " ").trim();
if (snippet.length() > 60) snippet = snippet.substring(0, 60);
mNote.setNoteValue(NoteColumns.SNIPPET, snippet);
// 提交修改到数据库
mNote.syncNote(mContext, mNoteId);
/**
* Update widget content if there exist any widget of this note
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
@ -377,43 +212,10 @@ public class WorkingNote {
}
}
/**
* HTML
* @param html HTML
* @return
*/
private String getPlainText(String html) {
if (TextUtils.isEmpty(html)) return "";
// 1. 处理块级标签,防止文字挤在一起
String result = html.replaceAll("(?i)<br\\s*/?>", "\n") // 换行符
.replaceAll("(?i)</div>", "\n") // 层叠样式结束换行
.replaceAll("(?i)</p>", "\n"); // 段落结束换行
// 2. 利用 Android 原生工具解析 HTML 实体 (如 &nbsp; -> 空格)
// 注意:这里使用 toString() 剥离大部分标准标签
result = android.text.Html.fromHtml(result).toString();
// 3. 正则强力保底清洗
result = result.replaceAll("<[^>]+>", "") // 删掉所有尖括号内的代码
.replaceAll("\\uFFFC", "") // 删掉 Android 的 ImageSpan 占位符(OBJECT_REPLACEMENT_CHARACTER)
.replaceAll("&[^;]+;", ""); // 删掉残留的 HTML 实体(如 &amp;
return result.trim();
}
/**
* 便
* @return truefalse
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
* 便
* @return truefalse
*/
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
@ -423,19 +225,10 @@ public class WorkingNote {
}
}
/**
* 便
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
*
* @param date
* @param set
*/
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date;
@ -446,10 +239,6 @@ public class WorkingNote {
}
}
/**
* 便
* @param mark
*/
public void markDeleted(boolean mark) {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
@ -458,18 +247,9 @@ public class WorkingNote {
}
}
/**
* ID
* @param id ID
*/
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id;
// 清除自定义背景
mCustomBgUri = "";
mNote.setNoteValue(NoteColumns.CUSTOM_BG_URI, "");
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
@ -477,28 +257,6 @@ public class WorkingNote {
}
}
/**
* URI
* @param uri URI
*/
public void setCustomBgUri(String uri) {
this.mCustomBgUri = uri;
// 立即告诉数据库也要更新这个值
mNote.setNoteValue(NoteColumns.CUSTOM_BG_URI, uri);
}
/**
* URI
* @return URI
*/
public String getCustomBgUri() {
return mCustomBgUri;
}
/**
*
* @param mode
*/
public void setCheckListMode(int mode) {
if (mMode != mode) {
if (mNoteSettingStatusListener != null) {
@ -509,10 +267,6 @@ public class WorkingNote {
}
}
/**
*
* @param type
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type;
@ -520,10 +274,6 @@ public class WorkingNote {
}
}
/**
* ID
* @param id ID
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id;
@ -531,10 +281,6 @@ public class WorkingNote {
}
}
/**
* 便
* @param text 便
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text;
@ -542,138 +288,80 @@ public class WorkingNote {
}
}
/**
* 便
* @param phoneNumber
* @param callDate
*/
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
/**
* 便
* @return truefalse
*/
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
/**
* 便
* @return 便
*/
public String getContent() {
return mContent;
}
/**
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
* @return ID
*/
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
/**
* ID
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
* @return ID
*/
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
/**
*
* @return
*/
public int getCheckListMode() {
return mMode;
}
/**
* 便ID
* @return 便ID
*/
public long getNoteId() {
return mNoteId;
}
/**
* ID
* @return ID
*/
public long getFolderId() {
return mFolderId;
}
/**
* ID
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* 便
*/
public interface NoteSettingChangedListener {
/**
* 便
* Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
*
* @param date
* @param set
* Called when user set clock
*/
void onClockAlertChanged(long date, boolean set);
/**
* 便
* Call when user create note from widget
*/
void onWidgetChanged();
/**
*
* @param oldMode
* @param newMode
* Call when switch between check list mode and normal mode
* @param oldMode is previous mode before change
* @param newMode is new mode
*/
void onCheckListModeChanged(int oldMode, int newMode);
}

@ -1,669 +0,0 @@
/**
* AI 怀
* <p>
* {@link Worker}
* 怀 Coze AI 怀
*/
package net.micode.notes.sync;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import com.google.gson.JsonObject;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.CloudNote;
import net.micode.notes.tool.AiNotificationHelper;
import net.micode.notes.tool.SyncMapper;
import net.micode.notes.tool.ai.AiDataSyncHelper;
import net.micode.notes.tool.ai.CozeClient;
import net.micode.notes.tool.ai.CozeRequest;
import net.micode.notes.tool.ai.CozeResponse;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class SyncWorker extends Worker {
/**
*
*/
private static final String TAG = "SyncWorker";
/**
*
*/
public static final String KEY_SYNC_MODE = "sync_mode";
/**
*
*/
public static final int MODE_ALL = 0;
/**
*
*/
public static final int MODE_PUSH = 1;
/**
*
*/
public static final int MODE_PULL = 2;
/**
*
*/
public static final int MODE_REMINDER = 3;
/**
* /
*/
public static final int MODE_RANDOM_CHAT = 4;
/**
*
*
* @param context
* @param workerParams
*/
public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
/**
*
* <p>
*
* <ul>
* <li>{@link #MODE_ALL}: </li>
* <li>{@link #MODE_PUSH}: </li>
* <li>{@link #MODE_PULL}: </li>
* <li>{@link #MODE_REMINDER}: AI</li>
* <li>{@link #MODE_RANDOM_CHAT}: AI怀</li>
* </ul>
*
* @return
*/
@NonNull
@Override
public Result doWork() {
// 准备一个锁,数量为 1
final CountDownLatch latch = new CountDownLatch(1);
// 用于记录是否发生错误
final boolean[] isSuccess = {true};
try {
String uid = FirebaseAuth.getInstance().getUid();
if (uid == null) return Result.success();
FirebaseFirestore db = FirebaseFirestore.getInstance();
int mode = getInputData().getInt(KEY_SYNC_MODE, MODE_ALL);
Log.d(TAG, "Starting Sync with mode: " + mode);
// --- PUSH 逻辑 (保持同步执行即可因为它本身不依赖回调返回数据给UI) ---
if (mode == MODE_PUSH || mode == MODE_ALL) {
performPush(db, uid);
syncToCozeAgent();
}
// --- PULL 逻辑 (异步变同步) ---
if (mode == MODE_PULL || mode == MODE_ALL) {
// 传递 latch 进去
performPull(db, uid, latch, isSuccess);
// [关键]:主线程在这里死等,直到 latch.countDown() 被调用,或者超时(10秒)
try {
latch.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
return Result.retry();
}
}
// 处理提醒模式
if (mode == MODE_REMINDER) {
String title = getInputData().getString("reminder_title");
requestAiReminderReply(title);
}
// 处理随机关怀模式
if (mode == MODE_RANDOM_CHAT) {
String type = getInputData().getString("care_type");
requestAiCareReply(type);
}
return isSuccess[0] ? Result.success() : Result.failure();
} catch (Exception e) {
Log.e(TAG, "General sync error: " + e.getMessage());
return Result.failure();
}
}
/**
* AI怀
* <p>
* 怀AI怀
*
* @param careType 怀
* <ul>
* <li>morning: </li>
* <li>night: </li>
* <li>: </li>
* </ul>
*/
private void requestAiCareReply(String careType) {
try {
// 1. 准备请求参数 (针对 Random 分支)
com.google.gson.JsonObject jsonPayload = new com.google.gson.JsonObject();
jsonPayload.addProperty("intent", "random");
// 拟人化 Prompt 引导
String prompt;
if ("morning".equals(careType)) {
prompt = "[System_Event]: 现在是早晨。请基于天气插件并结合我之前的笔记,给我一段充满活力的早安问候。";
} else if ("night".equals(careType)) {
prompt = "[System_Event]: 现在是深夜。请根据我的生活习惯提示我早点休息,语气要温柔。";
} else {
prompt = "[System_Event]: 现在是闲暇时刻。请从我的笔记库中挑一件有趣的事,或者分享一条今天的新闻来找我闲聊。";
}
jsonPayload.addProperty("payload", prompt);
jsonPayload.addProperty("current_time", AiDataSyncHelper.getCurrentTime());
// 2. 发起对话
CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", jsonPayload.toString());
retrofit2.Response<CozeResponse> response = CozeClient.getInstance()
.chat(CozeClient.getAuthToken(), request).execute();
String finalAnswer = null;
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
String chatId = response.body().data.id;
String convId = response.body().data.conversation_id;
// 3. 轮询状态
String status = "";
int retries = 0;
while (!"completed".equals(status) && retries < 15) {
Thread.sleep(2000);
retrofit2.Response<CozeResponse> pollResp = CozeClient.getInstance()
.retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute();
if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) {
status = pollResp.body().data.status;
}
retries++;
}
// 4. 状态完成后获取消息列表
if ("completed".equals(status)) {
retrofit2.Response<com.google.gson.JsonObject> msgListResp = CozeClient.getInstance()
.getMessageList(CozeClient.getAuthToken(), chatId, convId).execute();
if (msgListResp.isSuccessful() && msgListResp.body() != null) {
com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data");
for (com.google.gson.JsonElement el : messages) {
com.google.gson.JsonObject m = el.getAsJsonObject();
// 寻找 assistant 的真正 answer
if ("assistant".equals(m.get("role").getAsString()) &&
"answer".equals(m.get("type").getAsString())) {
String rawContent = m.get("content").getAsString();
finalAnswer = parseAiResponse(rawContent); // 强制脱壳
break;
}
}
}
}
}
// 5. 存储并通知用户
if (finalAnswer != null && !finalAnswer.isEmpty()) {
// 存入聊天表 (msg_type=0 代表普通气泡)
saveChatMessageToLocal(finalAnswer);
// 弹出顶部通知
AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer);
Log.d("AiCare", "Pushed care message: " + finalAnswer);
}
} catch (Exception e) {
Log.e("AiCare", "Failed to get care reply", e);
}
}
/**
* AI
* <p>
* AI
*
* @param agendaTitle
*/
private void requestAiReminderReply(String agendaTitle) {
try {
// 1. 构造请求参数
com.google.gson.JsonObject json = new com.google.gson.JsonObject();
json.addProperty("intent", "reminder");
json.addProperty("payload", "日程即将开始:" + agendaTitle);
json.addProperty("current_time", AiDataSyncHelper.getCurrentTime());
// 2. 发起对话
CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", json.toString());
retrofit2.Response<CozeResponse> response = CozeClient.getInstance()
.chat(CozeClient.getAuthToken(), request).execute();
// 声明变量,用于存放最终结果
String finalAnswer = null;
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
String chatId = response.body().data.id;
String convId = response.body().data.conversation_id;
// 3. 轮询状态直到完成
String status = "";
int retries = 0;
while (!"completed".equals(status) && retries < 15) {
Thread.sleep(2000);
retrofit2.Response<CozeResponse> pollResp = CozeClient.getInstance()
.retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute();
if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) {
status = pollResp.body().data.status;
}
retries++;
}
// 4. [核心修复点]:状态完成后,请求消息列表并解析出 content
if ("completed".equals(status)) {
retrofit2.Response<com.google.gson.JsonObject> msgListResp = CozeClient.getInstance()
.getMessageList(CozeClient.getAuthToken(), chatId, convId).execute();
if (msgListResp.isSuccessful() && msgListResp.body() != null) {
com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data");
// 遍历寻找 AI 的回答内容
for (com.google.gson.JsonElement el : messages) {
com.google.gson.JsonObject m = el.getAsJsonObject();
if ("assistant".equals(m.get("role").getAsString()) &&
"answer".equals(m.get("type").getAsString())) {
String rawContent = m.get("content").getAsString();
finalAnswer = parseAiResponse(rawContent); // 强制脱壳
break;
}
}
}
}
}
// 5. 统一处理最终结果
if (finalAnswer != null && !finalAnswer.isEmpty()) {
saveReminderToLocal(finalAnswer);
AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer);
Log.d("AiReminder", "Successfully got AI reminder: " + finalAnswer);
} else {
Log.w("AiReminder", "AI returned empty answer for reminder.");
}
} catch (Exception e) {
Log.e("AiReminder", "Error in requestAiReminderReply", e);
}
}
/**
*
* <p>
* AI
*
* @param content
*/
private void saveReminderToLocal(String content) {
android.content.ContentValues values = new android.content.ContentValues();
values.put(Notes.ChatColumns.SENDER_TYPE, 1); // AI
values.put(Notes.ChatColumns.MSG_TYPE, 1); // [重要] 设为提醒卡片类型
values.put(Notes.ChatColumns.CONTENT, content);
values.put(Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
getApplicationContext().getContentResolver().insert(
android.net.Uri.parse("content://micode_notes/chat_messages"), values);
}
/**
* Coze AI
* <p>
* Coze AIAI
*/
private void syncToCozeAgent() {
try {
com.google.gson.JsonObject payloadJson = new com.google.gson.JsonObject();
payloadJson.addProperty("intent", "sync");
payloadJson.addProperty("payload", AiDataSyncHelper.getSyncPayload(getApplicationContext()));
payloadJson.addProperty("current_time", AiDataSyncHelper.getCurrentTime());
CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", payloadJson.toString());
// 1. 发起对话
retrofit2.Response<CozeResponse> response = CozeClient.getInstance()
.chat(CozeClient.getAuthToken(), request).execute();
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
String chatId = response.body().data.id;
String convId = response.body().data.conversation_id; // [关键] 获取会话ID
String status = "";
int retries = 0;
while (!"completed".equals(status) && retries < 15) {
Thread.sleep(2000);
// [关键] 传入两个 ID 轮询
retrofit2.Response<CozeResponse> pollResp = CozeClient.getInstance()
.retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute();
if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) {
status = pollResp.body().data.status;
}
retries++;
}
// 2. 完成后抓取真正的“Answer”
if ("completed".equals(status)) {
retrofit2.Response<com.google.gson.JsonObject> msgListResp = CozeClient.getInstance()
.getMessageList(CozeClient.getAuthToken(), chatId, convId).execute();
if (msgListResp.isSuccessful() && msgListResp.body() != null) {
com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data");
String finalAnswer = null;
for (com.google.gson.JsonElement el : messages) {
com.google.gson.JsonObject m = el.getAsJsonObject();
if ("assistant".equals(m.get("role").getAsString()) && "answer".equals(m.get("type").getAsString())) {
finalAnswer = m.get("content").getAsString();
break;
}
}
if (finalAnswer != null) {
// --- 核心手术点:解析 AI 的决策 JSON ---
try {
com.google.gson.JsonObject responseObj = com.google.gson.JsonParser.parseString(finalAnswer).getAsJsonObject();
// 只有当 action 为 reply 时才处理
if (responseObj.has("action") && "reply".equals(responseObj.get("action").getAsString())) {
String assistantMsg = responseObj.get("content").getAsString();
// 1. 存入本地数据库 (以便用户进入聊天界面能看到)
saveChatMessageToLocal(assistantMsg);
// 2. [关键] 调用通知辅助类,弹出手机顶部通知
// 使用 getApplicationContext() 确保后台上下文安全
net.micode.notes.tool.AiNotificationHelper.sendAiNotification(
getApplicationContext(),
assistantMsg
);
android.util.Log.d("MiChatSync", "AI 决定主动关怀,已发送通知栏提醒");
}
} catch (Exception e) {
// 兜底逻辑:如果 AI 返回的不是 JSON (可能是纯文本),默认入库并通知
saveChatMessageToLocal(finalAnswer);
net.micode.notes.tool.AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer);
}
}
}
}
}
} catch (Exception e) {
Log.e("CozeSync", "Sync Failed", e);
}
}
/**
* AI
* <p>
* AI
*
* @param content
*/
private void saveChatMessageToLocal(String content) {
android.content.ContentValues values = new android.content.ContentValues();
values.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 1); // AI
values.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0); // 文本
values.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, content);
values.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
getApplicationContext().getContentResolver().insert(
android.net.Uri.parse("content://micode_notes/chat_messages"), values);
}
/**
*
* <p>
* sync_state = 1 Firebase Firestore
* serverIdUUIDserverId
* sync_state0
*
* @param db Firebase Firestore
* @param uid ID
*/
private void performPush(FirebaseFirestore db, String uid) {
Cursor cursor = getApplicationContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.SYNC_STATE + "=? AND " + NoteColumns.TYPE + "=? AND " + NoteColumns.ID + ">0",
new String[]{"1", String.valueOf(Notes.TYPE_NOTE)},
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
long localId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID));
String serverId = cursor.getString(cursor.getColumnIndex(NoteColumns.SERVER_ID));
// 如果没有 serverId说明是本地新建的分配一个 UUID
if (TextUtils.isEmpty(serverId)) {
serverId = UUID.randomUUID().toString();
}
// 获取完整正文 (跨表查询 data 表)
String fullContent = getNoteFullContent(localId);
// 翻译为云端对象
CloudNote cloudNote = SyncMapper.fromCursor(cursor, fullContent);
cloudNote.serverId = serverId;
// 上传 Firestore
final String finalServerId = serverId;
db.collection("users").document(uid)
.collection("notes").document(finalServerId)
.set(cloudNote)
.addOnSuccessListener(aVoid -> {
// 同步成功,回写本地状态为 0 (已同步)
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_STATE, 0);
values.put(NoteColumns.SERVER_ID, finalServerId);
getApplicationContext().getContentResolver().update(
Uri.withAppendedPath(Notes.CONTENT_NOTE_URI, String.valueOf(localId)),
values, null, null);
Log.d(TAG, "Push Success: " + finalServerId);
});
}
cursor.close();
}
}
/**
*
* <p>
* Firebase Firestore
*
*
* @param db Firebase Firestore
* @param uid ID
* @param latch 线
* @param isSuccess
*/
private void performPull(FirebaseFirestore db, String uid, CountDownLatch latch, boolean[] isSuccess) {
db.collection("users").document(uid).collection("notes")
.get()
.addOnSuccessListener(queryDocumentSnapshots -> {
try {
for (QueryDocumentSnapshot doc : queryDocumentSnapshots) {
CloudNote cloudNote = doc.toObject(CloudNote.class);
if (cloudNote != null) {
mergeCloudNoteToLocal(cloudNote);
}
}
Log.d(TAG, "Pull complete. Cloud docs processed.");
} catch (Exception e) {
Log.e(TAG, "Error merging data", e);
isSuccess[0] = false;
} finally {
// [关键]:通知主线程,活干完了,可以放行了
latch.countDown();
}
})
.addOnFailureListener(e -> {
Log.e(TAG, "Pull failed", e);
isSuccess[0] = false;
// [关键]:失败了也要通知放行,否则会死锁
latch.countDown();
});
}
/**
*
* <p>
*
* "待上传"
*
*
* @param cloudNote
*/
private void mergeCloudNoteToLocal(CloudNote cloudNote) {
Cursor c = getApplicationContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID, NoteColumns.MODIFIED_DATE, NoteColumns.SYNC_STATE},
NoteColumns.SERVER_ID + "=?",
new String[]{cloudNote.serverId},
null
);
if (c != null && c.moveToFirst()) {
// 本地已存在该便签
long localId = c.getLong(0);
long localModified = c.getLong(1);
int syncState = c.getInt(2);
// 只有当云端修改时间更新,且本地不是“待上传”状态时,才覆盖本地
if (cloudNote.modifiedDate > localModified && syncState == 0) {
updateLocalNote(localId, cloudNote);
}
c.close();
} else {
// 本地没有,直接新建
insertCloudNoteToLocal(cloudNote);
}
}
/**
*
* <p>
* data
*
* @param cloudNote
*/
private void insertCloudNoteToLocal(CloudNote cloudNote) {
ContentValues values = SyncMapper.toNoteValues(cloudNote);
Uri noteUri = getApplicationContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
if (noteUri != null) {
long newId = ContentUris.parseId(noteUri);
// 写入正文到 data 表
ContentValues dataValues = new ContentValues();
dataValues.put(Notes.DataColumns.NOTE_ID, newId);
dataValues.put(Notes.DataColumns.MIME_TYPE, Notes.TextNote.CONTENT_ITEM_TYPE);
dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
getApplicationContext().getContentResolver().insert(Notes.CONTENT_DATA_URI, dataValues);
}
}
/**
*
* <p>
* 使data
*
* @param localId ID
* @param cloudNote
*/
private void updateLocalNote(long localId, CloudNote cloudNote) {
ContentValues values = SyncMapper.toNoteValues(cloudNote);
getApplicationContext().getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, localId),
values, null, null);
ContentValues dataValues = new ContentValues();
dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
getApplicationContext().getContentResolver().update(
Notes.CONTENT_DATA_URI,
dataValues,
Notes.DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(localId)});
}
/**
*
* <p>
* IDdata
*
* @param noteId ID
* @return
*/
private String getNoteFullContent(long noteId) {
Cursor c = getApplicationContext().getContentResolver().query(
Notes.CONTENT_DATA_URI,
new String[]{Notes.DataColumns.CONTENT},
Notes.DataColumns.NOTE_ID + "=? AND " + Notes.DataColumns.MIME_TYPE + "=?",
new String[]{String.valueOf(noteId), Notes.TextNote.CONTENT_ITEM_TYPE},
null
);
String content = "";
if (c != null) {
if (c.moveToFirst()) content = c.getString(0);
c.close();
}
return content;
}
/**
* AI
* <p>
* AIcontent
* JSON
*
* @param rawResponse
* @return
*/
private String parseAiResponse(String rawResponse) {
if (rawResponse == null || !rawResponse.trim().startsWith("{")) {
return rawResponse; // 如果不是JSON直接返回原样数据
}
try {
com.google.gson.JsonObject jsonObject = com.google.gson.JsonParser.parseString(rawResponse).getAsJsonObject();
if (jsonObject.has("content")) {
return jsonObject.get("content").getAsString();
}
} catch (Exception e) {
android.util.Log.e("AiSync", "JSON解析失败保留原样", e);
}
return rawResponse;
}
}

@ -1,88 +0,0 @@
/**
* AI
* <p>
* AI
* Android 8.0+
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
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.NotesListActivity;
public class AiNotificationHelper {
/**
* ID
*/
private static final String CHANNEL_ID = "mi_chat_channel";
/**
*
*/
private static final String CHANNEL_NAME = "Mi Chat AI Assistant";
/**
* ID
*/
private static final int NOTIFICATION_ID = 1001;
/**
* AI
* <p>
* AINotesListActivityChatFragment
* </p>
* @param context
* @param content
*/
public static void sendAiNotification(Context context, String content) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 1. 创建通知渠道 (Android 8.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH // 重要通知,会弹出
);
channel.setDescription("Notifications from AI Assistant");
channel.enableLights(true);
channel.enableVibration(true);
manager.createNotificationChannel(channel);
}
// 2. 设置点击跳转意图
// 跳转到 NotesListActivity并携带参数指引它打开 ChatFragment
Intent intent = new Intent(context, NotesListActivity.class);
intent.putExtra("open_nav_ai", true); // 这个 Extra 需要在 NotesListActivity 中处理
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 3. 构建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.icon_app) // 确保有这个资源
.setContentTitle("Mi Chat")
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setDefaults(Notification.DEFAULT_ALL);
// 4. 发送
manager.notify(NOTIFICATION_ID, builder.build());
}
}

@ -14,14 +14,6 @@
* limitations under the License.
*/
/**
*
* <p>
* 便便
*
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.content.Context;
@ -45,24 +37,10 @@ import java.io.PrintStream;
public class BackupUtils {
/**
*
*/
private static final String TAG = "BackupUtils";
/**
*
*/
// Singleton stuff
private static BackupUtils sInstance;
/**
* BackupUtils
* <p>
*
* </p>
* @param context
* @return BackupUtils
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
sInstance = new BackupUtils(context);
@ -71,74 +49,43 @@ public class BackupUtils {
}
/**
*
* Following states are signs to represents backup or restore
* status
*/
// SD卡未挂载
// Currently, the sdcard is not mounted
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// 备份文件不存在
// The backup file not exist
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// 数据格式不正确,可能被其他程序修改
// The data is not well formated, may be changed by other programs
public static final int STATE_DATA_DESTROIED = 2;
// 运行时异常导致备份或恢复失败
// Some run-time exception which causes restore or backup fails
public static final int STATE_SYSTEM_ERROR = 3;
// 备份或恢复成功
// Backup or restore success
public static final int STATE_SUCCESS = 4;
/**
*
*/
private TextExport mTextExport;
/**
*
* @param context
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
/**
*
* @return
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 便
* @return 使STATE_*
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
* @return
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
* <p>
* 便便
* </p>
*/
private static class TextExport {
/**
* 便
*/
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
@ -146,24 +93,12 @@ public class BackupUtils {
NoteColumns.TYPE
};
/**
* 便ID
*/
private static final int NOTE_COLUMN_ID = 0;
/**
* 便
*/
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
/**
* 便
*/
private static final int NOTE_COLUMN_SNIPPET = 2;
/**
*
*/
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
@ -173,65 +108,23 @@ public class BackupUtils {
DataColumns.DATA4,
};
/**
*
*/
private static final int DATA_COLUMN_CONTENT = 0;
/**
* MIME
*/
private static final int DATA_COLUMN_MIME_TYPE = 1;
/**
*
*/
private static final int DATA_COLUMN_CALL_DATE = 2;
/**
*
*/
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
/**
*
*/
private final String [] TEXT_FORMAT;
/**
*
*/
private static final int FORMAT_FOLDER_NAME = 0;
/**
* 便
*/
private static final int FORMAT_NOTE_DATE = 1;
/**
* 便
*/
private static final int FORMAT_NOTE_CONTENT = 2;
/**
*
*/
private Context mContext;
/**
*
*/
private String mFileName;
/**
*
*/
private String mFileDirectory;
/**
*
* @param context
*/
public TextExport(Context context) {
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
@ -239,22 +132,15 @@ public class BackupUtils {
mFileDirectory = "";
}
/**
*
* @param id
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
*
* @param folderId ID
* @param ps
* Export the folder identified by folder id to text
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询属于此文件夹的便签
// Query notes belong to this folder
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
folderId
@ -263,11 +149,11 @@ public class BackupUtils {
if (notesCursor != null) {
if (notesCursor.moveToFirst()) {
do {
// 打印便签的最后修改日期
// Print note's last modified date
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 查询属于此便签的数据
// Query data belong to this note
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
@ -277,9 +163,7 @@ public class BackupUtils {
}
/**
* 便
* @param noteId 便ID
* @param ps
* Export note identified by id to a print stream
*/
private void exportNoteToText(String noteId, PrintStream ps) {
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
@ -292,7 +176,7 @@ public class BackupUtils {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// 打印电话号码
// Print phone number
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
@ -301,11 +185,11 @@ public class BackupUtils {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
phoneNumber));
}
// 打印通话日期
// Print call date
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate)));
// 打印通话附件位置
// Print call attachment location
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
location));
@ -321,10 +205,10 @@ public class BackupUtils {
}
dataCursor.close();
}
// 在便签之间打印行分隔符
// print a line separator between note
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LINE_SEPARATOR
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
});
} catch (IOException e) {
Log.e(TAG, e.toString());
@ -332,8 +216,7 @@ public class BackupUtils {
}
/**
* 便
* @return 使STATE_*
* Note will be exported as text which is user readable
*/
public int exportToText() {
if (!externalStorageAvailable()) {
@ -346,7 +229,7 @@ public class BackupUtils {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// 首先导出文件夹及其便签
// First export folder and its notes
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
@ -357,7 +240,7 @@ public class BackupUtils {
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 打印文件夹名称
// Print folder's name
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
@ -374,7 +257,7 @@ public class BackupUtils {
folderCursor.close();
}
// 导出根文件夹中的便签
// Export notes in root's folder
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
@ -387,7 +270,7 @@ public class BackupUtils {
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 查询属于此便签的数据
// Query data belong to this note
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
@ -400,8 +283,7 @@ public class BackupUtils {
}
/**
*
* @return
* Get a print stream pointed to the file {@generateExportedTextFile}
*/
private PrintStream getExportToTextPrintStream() {
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
@ -428,11 +310,7 @@ public class BackupUtils {
}
/**
*
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return
* Generate the text file to store imported data
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();

@ -14,13 +14,6 @@
* limitations under the License.
*/
/**
*
* <p>
* 便
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.content.ContentProviderOperation;
@ -36,45 +29,14 @@ import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.NoteColumns;
// import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
public class DataUtils {
/**
*
*/
public static final String TAG = "DataUtils";
/**
*
* <p>
* ID
* </p>
*/
public static class AppWidgetAttribute {
/**
* ID
*/
public int widgetId;
/**
*
*/
public int widgetType;
}
/**
* 便
* <p>
* ContentProviderID便
* </p>
* @param resolver ContentResolver
* @param ids 便ID
* @return
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
@ -110,16 +72,6 @@ public class DataUtils {
return false;
}
/**
* 便
* <p>
* 便IDID
* </p>
* @param resolver ContentResolver
* @param id 便ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId);
@ -128,16 +80,6 @@ public class DataUtils {
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
* 便
* <p>
* ContentProvider便ID
* </p>
* @param resolver ContentResolver
* @param ids 便ID
* @param folderId ID
* @return
*/
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
if (ids == null) {
@ -170,12 +112,7 @@ public class DataUtils {
}
/**
*
* <p>
*
* </p>
* @param resolver ContentResolver
* @return
* Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}}
*/
public static int getUserFolderCount(ContentResolver resolver) {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
@ -199,16 +136,6 @@ public class DataUtils {
return count;
}
/**
* 便
* <p>
* ID便
* </p>
* @param resolver ContentResolver
* @param noteId 便ID
* @param type 便
* @return 便
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
@ -226,15 +153,6 @@ public class DataUtils {
return exist;
}
/**
* 便
* <p>
* ID便
* </p>
* @param resolver ContentResolver
* @param noteId 便ID
* @return 便
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
@ -249,15 +167,6 @@ public class DataUtils {
return exist;
}
/**
*
* <p>
* ID
* </p>
* @param resolver ContentResolver
* @param dataId ID
* @return
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
@ -272,15 +181,6 @@ public class DataUtils {
return exist;
}
/**
*
* <p>
*
* </p>
* @param resolver ContentResolver
* @param name
* @return
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
@ -297,15 +197,6 @@ public class DataUtils {
return exist;
}
/**
* 便
* <p>
* 便ID
* </p>
* @param resolver ContentResolver
* @param folderId ID
* @return
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
@ -333,15 +224,6 @@ public class DataUtils {
return set;
}
/**
* 便ID
* <p>
* 便
* </p>
* @param resolver ContentResolver
* @param noteId 便ID
* @return
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
@ -361,16 +243,6 @@ public class DataUtils {
return "";
}
/**
* 便ID
* <p>
* 便
* </p>
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return 便ID
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
@ -392,16 +264,6 @@ public class DataUtils {
return 0;
}
/**
* ID便
* <p>
* 便
* </p>
* @param resolver ContentResolver
* @param noteId 便ID
* @return 便
* @throws IllegalArgumentException 便
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
@ -420,14 +282,6 @@ public class DataUtils {
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
* 便
* <p>
*
* </p>
* @param snippet
* @return
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim();
@ -438,136 +292,4 @@ public class DataUtils {
}
return snippet;
}
/**
*
* <p>
*
* </p>
* @param resolver ContentResolver
* @param ids 便ID
* @param column
* @param value
* @return
*/
public static boolean batchUpdateField(ContentResolver resolver, HashSet<Long> ids, String column, int value) {
if (ids == null || ids.size() == 0) {
Log.d(TAG, "No ids to update");
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't update system folder root");
continue;
}
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(column, value);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "Update notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
/**
*
* <p>
* 便
* </p>
* @param resolver ContentResolver
* @param ids 便ID
* @return
*/
public static boolean batchMoveToTrash(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null || ids.size() == 0) return true;
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
// 排除系统文件夹
if(id == Notes.ID_ROOT_FOLDER || id == Notes.ID_CALL_RECORD_FOLDER) continue;
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 将当前 parent_id 备份到 origin_parent_id
// 注意:这里利用 SQLite 的特性,直接让 origin_parent_id = parent_id
// 但 ContentProviderOperation 只能传值。
// 由于批量操作可能来自不同文件夹,标准做法是先查后改,或者依赖 Database Trigger。
// 鉴于本项目 database trigger 较复杂,我们这里简化处理:
// 在 NotesListActivity 中调用此方法前,通常只针对当前文件夹下的便签。
// 更好的方式是让 SQL 执行: UPDATE note SET origin_parent_id=parent_id, parent_id=-3 WHERE id=...
// 但 Android Provider 限制了灵活性。我们这里采用直接移动到 Trash。
// 注意NotesDatabaseHelper 中已有 Trigger `folder_move_notes_on_trash`
// 但它只处理文件夹移动。
builder.withValue(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
// 暂时不通过代码强制写 origin_parent_id假设后续还原时如果 origin 为 0 则回根目录
// 若要完美支持,需修改 Provider 支持 raw SQL或在此处先查询。
// 简单实现:只移动,不记录 origin (还原时默认回根目录),或者依赖 NoteEditActivity 保存时的逻辑。
// 考虑到项目架构老旧我们采用Move to Trash = parent_id 设为 -3
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
// ... (执行 batch代码与 batchDeleteNotes 类似)
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
return (results != null && results.length > 0);
} catch (Exception e) {
Log.e(TAG, e.toString());
return false;
}
}
/**
*
* <p>
* 便
* </p>
* @param resolver ContentResolver
* @param ids 便ID
* @return
*/
public static boolean batchRestoreFromTrash(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null || ids.size() == 0) return true;
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 还原逻辑:
// 理想情况SET parent_id = origin_parent_id
// 现实情况:由于 origin_parent_id 可能未正确维护,我们统一还原到 根文件夹 (ID_ROOT_FOLDER)
// 除非我们确信 origin_parent_id 有值。
// 为了稳妥,这里硬编码还原到 DEFAULT 文件夹,符合"直接回到删除之前"的降级体验
// 如果要完美,需要先 query origin_parent_id。
builder.withValue(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
return (results != null && results.length > 0);
} catch (Exception e) {
Log.e(TAG, e.toString());
return false;
}
}
}

@ -1,270 +0,0 @@
/**
* DeepSeek AI
* <p>
* DeepSeek API
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class DeepSeekHelper {
/**
*
*/
private static final String TAG = "DeepSeekHelper";
/**
* API
*/
private static final String API_KEY = "sk-6fd917bdb96a48fba119dc64e58bf458";
/**
* API URL
*/
private static final String API_URL = "https://api.deepseek.com/chat/completions";
/**
* AI
* <p>
* AI
* </p>
*/
public interface AICallback {
/**
*
* @param results
*/
void onSuccess(List<AgendaResult> results);
/**
*
* @param error
*/
void onFailure(String error);
}
/**
*
* <p>
*
* </p>
*/
public static class AgendaResult {
/**
*
*/
public String title;
/**
*
*/
public long startTime;
/**
*
*/
public long endTime;
/**
* "16:00", "下周", "12月", "全天"
*/
public String timeLabel;
/**
*
* @param title
* @param startTime
* @param endTime
* @param timeLabel
*/
public AgendaResult(String title, long startTime, long endTime, String timeLabel) {
this.title = title;
this.startTime = startTime;
this.endTime = endTime;
this.timeLabel = timeLabel;
}
}
/**
*
* <p>
* DeepSeek API
* </p>
* @param content
* @param callback
*/
public static void analyzeContent(String content, AICallback callback) {
new AnalyzeTask(content, callback).execute();
}
/**
*
* <p>
* AI HTTP DeepSeek API
* </p>
*/
private static class AnalyzeTask extends AsyncTask<Void, Void, String> {
/**
*
*/
private String content;
/**
*
*/
private AICallback callback;
/**
*
*/
private List<AgendaResult> parsedResults = new ArrayList<>();
/**
*
* @param content
* @param callback
*/
public AnalyzeTask(String content, AICallback callback) {
this.content = content;
this.callback = callback;
}
/**
*
* @param voids
* @return null
*/
@Override
protected String doInBackground(Void... voids) {
try {
// 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd EEEE HH:mm", Locale.CHINA);
String currentTime = sdf.format(new Date());
// [核心优化] 更加智能的 Prompt
String systemPrompt =
"Current Time: " + currentTime + ".\n" +
"Role: You are a strict JSON schedule parser.\n" +
"Task: Extract unique events from user text.\n" +
"Rules:\n" +
"1. ONE EVENT = ONE JSON OBJECT. Never duplicate an event.\n" +
"2. If an event has a specific time (e.g. 8:00), set 'start' and 'end' to that exact timestamp.\n" +
"3. If an event is for a whole day, set 'start' to 00:00 and 'end' to 23:59 of that day.\n" +
"4. Format: [{'title': string, 'start': 'yyyy-MM-dd HH:mm', 'end': 'yyyy-MM-dd HH:mm', 'label': string}].\n" +
"5. The 'label' MUST be either 'HH:mm' for specific points, or '全天', '本周', '本月'.\n" +
"No explanations, return ONLY JSON.";
// 3. 构建 JSON Body
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", content);
JSONObject sysMsg = new JSONObject();
sysMsg.put("role", "system");
sysMsg.put("content", systemPrompt);
JSONArray messages = new JSONArray();
messages.put(sysMsg);
messages.put(userMsg);
JSONObject jsonBody = new JSONObject();
jsonBody.put("model", "deepseek-chat");
jsonBody.put("messages", messages);
jsonBody.put("temperature", 0.1); // 低温度以保证格式稳定
// 4. 发起 HTTP 请求
URL url = new URL(API_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
conn.setDoOutput(true);
try(OutputStream os = conn.getOutputStream()) {
byte[] input = jsonBody.toString().getBytes("utf-8");
os.write(input, 0, input.length);
}
// 5. 读取响应
int code = conn.getResponseCode();
if (code == 200) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
StringBuilder response = new StringBuilder();
String responseLine = null;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
// 6. 解析 DeepSeek 返回的 JSON
JSONObject responseJson = new JSONObject(response.toString());
String aiContent = responseJson.getJSONArray("choices")
.getJSONObject(0).getJSONObject("message").getString("content");
// 清洗可能存在的 Markdown 代码块标记 ```json ... ```
if (aiContent.startsWith("```")) {
aiContent = aiContent.replaceAll("```json", "").replaceAll("```", "");
}
// 更新解析逻辑
JSONArray events = new JSONArray(aiContent.trim());
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
for (int i = 0; i < events.length(); i++) {
JSONObject event = events.getJSONObject(i);
String title = event.getString("title");
String startStr = event.getString("start");
String endStr = event.getString("end");
String label = event.getString("label");
Date start = parser.parse(startStr);
Date end = parser.parse(endStr);
if (start != null && end != null) {
parsedResults.add(new AgendaResult(title, start.getTime(), end.getTime(), label));
}
}
return null; // Success
} else {
return "Error Code: " + code;
}
} catch (Exception e) {
Log.e(TAG, "AI Request Failed", e);
return e.getMessage();
}
}
/**
*
* @param error null
*/
@Override
protected void onPostExecute(String error) {
if (error == null) {
if (parsedResults.isEmpty()) {
callback.onFailure("未识别到包含时间的日程信息");
} else {
callback.onSuccess(parsedResults);
}
} else {
callback.onFailure("AI 服务连接失败: " + error);
}
}
}
}

@ -14,245 +14,100 @@
* limitations under the License.
*/
/**
* GTask
* <p>
* GTask JSON
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
public class GTaskStringUtils {
/**
* ID
*/
public final static String GTASK_JSON_ACTION_ID = "action_id";
/**
*
*/
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/**
*
*/
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
/**
*
*/
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
/**
*
*/
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
/**
*
*/
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
/**
*
*/
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
/**
* ID
*/
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/**
*
*/
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/**
*
*/
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
/**
*
*/
public final static String GTASK_JSON_COMPLETED = "completed";
/**
* ID
*/
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/**
* ID
*/
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/**
*
*/
public final static String GTASK_JSON_DELETED = "deleted";
/**
*
*/
public final static String GTASK_JSON_DEST_LIST = "dest_list";
/**
*
*/
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/**
*
*/
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
/**
*
*/
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/**
*
*/
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
/**
*
*/
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
/**
* ID
*/
public final static String GTASK_JSON_ID = "id";
/**
*
*/
public final static String GTASK_JSON_INDEX = "index";
/**
*
*/
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/**
*
*/
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
/**
* ID
*/
public final static String GTASK_JSON_LIST_ID = "list_id";
/**
*
*/
public final static String GTASK_JSON_LISTS = "lists";
/**
*
*/
public final static String GTASK_JSON_NAME = "name";
/**
* ID
*/
public final static String GTASK_JSON_NEW_ID = "new_id";
/**
*
*/
public final static String GTASK_JSON_NOTES = "notes";
/**
* ID
*/
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/**
* ID
*/
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
/**
*
*/
public final static String GTASK_JSON_RESULTS = "results";
/**
*
*/
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/**
*
*/
public final static String GTASK_JSON_TASKS = "tasks";
/**
*
*/
public final static String GTASK_JSON_TYPE = "type";
/**
*
*/
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/**
*
*/
public final static String GTASK_JSON_TYPE_TASK = "TASK";
/**
*
*/
public final static String GTASK_JSON_USER = "user";
/**
* MIUI
*/
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/**
*
*/
public final static String FOLDER_DEFAULT = "Default";
/**
*
*/
public final static String FOLDER_CALL_NOTE = "Call_Note";
/**
*
*/
public final static String FOLDER_META = "METADATA";
/**
* GTask ID
*/
public final static String META_HEAD_GTASK_ID = "meta_gid";
/**
*
*/
public final static String META_HEAD_NOTE = "meta_note";
/**
*
*/
public final static String META_HEAD_DATA = "meta_data";
/**
*
*/
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}

@ -14,13 +14,6 @@
* limitations under the License.
*/
/**
*
* <p>
* 便ID
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.content.Context;
@ -31,71 +24,22 @@ import net.micode.notes.ui.NotesPreferenceActivity;
public class ResourceParser {
/**
*
*/
public static final int YELLOW = 0;
/**
*
*/
public static final int BLUE = 1;
/**
*
*/
public static final int WHITE = 2;
/**
* 绿
*/
public static final int GREEN = 3;
/**
*
*/
public static final int RED = 4;
/**
*
*/
public static final int BG_DEFAULT_COLOR = YELLOW;
/**
*
*/
public static final int TEXT_SMALL = 0;
/**
*
*/
public static final int TEXT_MEDIUM = 1;
/**
*
*/
public static final int TEXT_LARGE = 2;
/**
*
*/
public static final int TEXT_SUPER = 3;
/**
*
*/
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
* 便
* <p>
* 便
* </p>
*/
public static class NoteBgResources {
/**
*
*/
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
@ -104,9 +48,6 @@ public class ResourceParser {
R.drawable.edit_red
};
/**
*
*/
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
@ -115,34 +56,15 @@ public class ResourceParser {
R.drawable.edit_title_red
};
/**
* 便
* @param id ID
* @return ID
*/
public static int getNoteBgResource(int id) {
return BG_EDIT_RESOURCES[id];
}
/**
* 便
* @param id ID
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
}
}
/**
* ID
* <p>
* ID
* ID
* </p>
* @param context
* @return ID
*/
public static int getDefaultBgId(Context context) {
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
@ -152,16 +74,7 @@ public class ResourceParser {
}
}
/**
* 便
* <p>
* 便便
* </p>
*/
public static class NoteItemBgResources {
/**
*
*/
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
@ -170,9 +83,6 @@ public class ResourceParser {
R.drawable.list_red_up
};
/**
*
*/
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
@ -181,9 +91,6 @@ public class ResourceParser {
R.drawable.list_red_middle
};
/**
*
*/
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
@ -192,9 +99,6 @@ public class ResourceParser {
R.drawable.list_red_down,
};
/**
*
*/
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
@ -203,61 +107,28 @@ public class ResourceParser {
R.drawable.list_red_single
};
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
return BG_FIRST_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgLastRes(int id) {
return BG_LAST_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
return BG_SINGLE_RESOURCES[id];
}
/**
*
* @param id ID
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
return BG_NORMAL_RESOURCES[id];
}
/**
*
* @return ID
*/
public static int getFolderBgRes() {
return R.drawable.list_folder;
}
}
/**
*
* <p>
* 便
* </p>
*/
public static class WidgetBgResources {
/**
* 2x
*/
private final static int [] BG_2X_RESOURCES = new int [] {
R.drawable.widget_2x_yellow,
R.drawable.widget_2x_blue,
@ -266,18 +137,10 @@ public class ResourceParser {
R.drawable.widget_2x_red,
};
/**
* 2x
* @param id ID
* @return ID
*/
public static int getWidget2xBgResource(int id) {
return BG_2X_RESOURCES[id];
}
/**
* 4x
*/
private final static int [] BG_4X_RESOURCES = new int [] {
R.drawable.widget_4x_yellow,
R.drawable.widget_4x_blue,
@ -286,26 +149,12 @@ public class ResourceParser {
R.drawable.widget_4x_red
};
/**
* 4x
* @param id ID
* @return ID
*/
public static int getWidget4xBgResource(int id) {
return BG_4X_RESOURCES[id];
}
}
/**
*
* <p>
*
* </p>
*/
public static class TextAppearanceResources {
/**
*
*/
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
@ -313,15 +162,6 @@ public class ResourceParser {
R.style.TextAppearanceSuper
};
/**
*
* <p>
* ID
* ID
* </p>
* @param id ID
* @return ID
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: Fix bug of store the resource id in shared preference.
@ -334,10 +174,6 @@ public class ResourceParser {
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}

@ -1,74 +0,0 @@
/**
*
* <p>
*
* </p>
* @since 1.0
*/
package net.micode.notes.tool;
import android.content.ContentValues;
import android.database.Cursor;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.CloudNote;
public class SyncMapper {
/**
* CloudNote
* <p>
* Cursor
* Cursor
* </p>
* @param cursor
* @param fullContent data
* @return CloudNote
*/
public static CloudNote fromCursor(Cursor cursor, String fullContent) {
CloudNote cloudNote = new CloudNote();
// 映射字段
cloudNote.serverId = cursor.getString(cursor.getColumnIndex(NoteColumns.SERVER_ID));
cloudNote.title = cursor.getString(cursor.getColumnIndex(NoteColumns.TITLE));
cloudNote.modifiedDate = cursor.getLong(cursor.getColumnIndex(NoteColumns.MODIFIED_DATE));
cloudNote.bgColorId = cursor.getInt(cursor.getColumnIndex(NoteColumns.BG_COLOR_ID));
cloudNote.isAgenda = cursor.getInt(cursor.getColumnIndex(NoteColumns.IS_AGENDA));
cloudNote.agendaDate = cursor.getLong(cursor.getColumnIndex(NoteColumns.AGENDA_DATE));
cloudNote.isCompleted = cursor.getInt(cursor.getColumnIndex(NoteColumns.IS_COMPLETED));
// 重要content 是从 data 表中查询出的完整字符串
cloudNote.content = fullContent;
return cloudNote;
}
/**
* CloudNoteContentValues
* <p>
* CloudNoteContentValues
* </p>
* @param cloudNote
* @return ContentValues
*/
public static ContentValues toNoteValues(CloudNote cloudNote) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SERVER_ID, cloudNote.serverId);
values.put(NoteColumns.TITLE, cloudNote.title);
// 云端同步下来的数据,状态设为"已同步 (0)"
values.put(NoteColumns.SYNC_STATE, 0);
values.put(NoteColumns.MODIFIED_DATE, cloudNote.modifiedDate);
values.put(NoteColumns.BG_COLOR_ID, cloudNote.bgColorId);
values.put(NoteColumns.IS_AGENDA, cloudNote.isAgenda);
values.put(NoteColumns.AGENDA_DATE, cloudNote.agendaDate);
values.put(NoteColumns.IS_COMPLETED, cloudNote.isCompleted);
// 摘要处理:取正文前 60 个字符存入 snippet 字段用于列表展示
String snippet = cloudNote.content;
if (snippet != null && snippet.length() > 60) {
snippet = snippet.substring(0, 60);
}
values.put(NoteColumns.SNIPPET, snippet);
return values;
}
}

@ -1,83 +0,0 @@
/**
* AI怀
* <p>
* AI怀
* 怀
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import java.util.Calendar;
import java.util.Random;
public class AiCareScheduler {
/**
* AI怀广
*/
public static final String ACTION_AI_CARE = "net.micode.notes.ACTION_AI_CARE";
/**
* 怀
* <p>
* App 怀
* <ul>
* <li>7:30</li>
* <li>22:30</li>
* <li>10:00-18:002</li>
* </ul>
* </p>
* @param context
*/
public static void scheduleDailyCare(Context context) {
// 1. 设置固定时间点:早安 (07:30) 和 晚安 (22:30)
setCareAlarm(context, 7, 30, "morning", 10001);
setCareAlarm(context, 22, 30, "night", 10002);
// 2. 设置伪随机时间点:在 10:00 - 18:00 之间随机选 2 个点
Random random = new Random();
for (int i = 0; i < 2; i++) {
int hour = 10 + random.nextInt(8); // 10-17点
int minute = random.nextInt(60);
setCareAlarm(context, hour, minute, "random", 20001 + i);
}
}
/**
* 怀
* <p>
* 广AiReminderReceiver
* </p>
* @param context
* @param hour 24
* @param minute
* @param type 怀morning/night/random
* @param requestCode
*/
private static void setCareAlarm(Context context, int hour, int minute, String type, int requestCode) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, minute);
cal.set(Calendar.SECOND, 0);
// 如果设定的时间已经过了,就设为明天
if (cal.getTimeInMillis() < System.currentTimeMillis()) {
cal.add(Calendar.DAY_OF_YEAR, 1);
}
Intent intent = new Intent(context, AiReminderReceiver.class); // 复用 Receiver
intent.setAction(ACTION_AI_CARE);
intent.putExtra("care_type", type);
PendingIntent pi = PendingIntent.getBroadcast(context, requestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// 使用非精准闹钟即可(省电且不需要秒级精确)
am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), pi);
}
}

@ -1,83 +0,0 @@
/**
* AI
* <p>
* AI便
*
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class AiDataSyncHelper {
/**
*
* <p>
*
* <ul>
* <li>10便</li>
* <li>IS_AGENDA=1便</li>
* </ul>
* </p>
* @param context
* @return
*/
public static String getSyncPayload(Context context) {
ContentResolver cr = context.getContentResolver();
StringBuilder sb = new StringBuilder();
// 1. 提取最近修改的便签 (取前10条以防Prompt过长)
Cursor cursor = cr.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, null, NoteColumns.MODIFIED_DATE + " DESC LIMIT 10");
sb.append("【用户便签改动】\n");
if (cursor != null) {
while (cursor.moveToNext()) {
String title = cursor.getString(cursor.getColumnIndex(NoteColumns.TITLE));
String content = cursor.getString(cursor.getColumnIndex(NoteColumns.SNIPPET));
long time = cursor.getLong(cursor.getColumnIndex(NoteColumns.MODIFIED_DATE));
sb.append("- 标题: " + (title != null ? title : "无")
+ " | 内容: " + content
+ " | 时间: " + new Date(time).toString() + "\n");
}
cursor.close();
}
// 2. 提取日程安排 (IS_AGENDA = 1)
Cursor agendaCursor = cr.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.IS_AGENDA + "=1", null, null);
sb.append("\n【当前日程安排】\n");
if (agendaCursor != null) {
while (agendaCursor.moveToNext()) {
String snippet = agendaCursor.getString(agendaCursor.getColumnIndex(NoteColumns.SNIPPET));
long date = agendaCursor.getLong(agendaCursor.getColumnIndex(NoteColumns.AGENDA_DATE));
sb.append("- 事项: " + snippet
+ " | 预定时间: " + new Date(date).toString() + "\n");
}
agendaCursor.close();
}
return sb.toString();
}
/**
*
* <p>
* yyyy-MM-dd HH:mm:ss
* </p>
* @return
*/
public static String getCurrentTime() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date());
}
}

@ -1,70 +0,0 @@
/**
* AI广
* <p>
* AI广
* <ul>
* <li>AiReminderScheduler</li>
* <li>怀AiCareScheduler</li>
* </ul>
* 广WorkManager
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import net.micode.notes.sync.SyncWorker;
public class AiReminderReceiver extends BroadcastReceiver {
/**
* 广AI
* <p>
* 广WorkManager
* <ul>
* <li>MODE_REMINDER</li>
* <li>怀MODE_RANDOM_CHAT</li>
* </ul>
* </p>
* @param context
* @param intent 广Intent
*/
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) return;
Log.d("AiAssistant", "Receiver caught action: " + action);
Data.Builder dataBuilder = new Data.Builder();
// 分支 1处理精准日程提醒
if (AiReminderScheduler.ACTION_AI_REMINDER.equals(action)) {
String title = intent.getStringExtra("title");
Log.d("AiReminder", "Alarm trigger for: " + title);
dataBuilder.putInt(SyncWorker.KEY_SYNC_MODE, SyncWorker.MODE_REMINDER);
dataBuilder.putString("reminder_title", title);
}
// 分支 2处理主动关怀闲聊
else if (AiCareScheduler.ACTION_AI_CARE.equals(action)) {
String careType = intent.getStringExtra("care_type");
Log.d("AiCare", "Care alarm trigger! Type: " + careType);
dataBuilder.putInt(SyncWorker.KEY_SYNC_MODE, SyncWorker.MODE_RANDOM_CHAT);
dataBuilder.putString("care_type", careType);
}
// 统一封装并启动 WorkManager
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SyncWorker.class)
.setInputData(dataBuilder.build())
.build();
WorkManager.getInstance(context).enqueue(request);
}
}

@ -1,136 +0,0 @@
/**
* AI
* <p>
*
* /
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import java.util.Calendar;
public class AiReminderScheduler {
/**
* AI广
*/
public static final String ACTION_AI_REMINDER = "net.micode.notes.ACTION_AI_REMINDER";
/**
*
* <p>
*
*
* </p>
* @param context
*/
public static void updateAllReminders(Context context) {
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// 1. 查询所有有效日程 (未完成且是日程)
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
null, NoteColumns.IS_AGENDA + "=1 AND " + NoteColumns.IS_COMPLETED + "=0",
null, null);
if (c != null) {
while (c.moveToNext()) {
long noteId = c.getLong(c.getColumnIndex(NoteColumns.ID));
String title = c.getString(c.getColumnIndex(NoteColumns.SNIPPET));
long startTime = c.getLong(c.getColumnIndex(NoteColumns.AGENDA_DATE));
long endTime = c.getLong(c.getColumnIndex(NoteColumns.AGENDA_END_DATE));
String timeLabel = c.getString(c.getColumnIndex(NoteColumns.TIME_LABEL));
long triggerTime = calculateTriggerTime(startTime, timeLabel);
// 只有未来的时间才设闹钟
if (triggerTime > System.currentTimeMillis()) {
setExactAlarm(context, am, noteId, title, triggerTime);
}
}
c.close();
}
}
/**
*
* <p>
*
* <ul>
* <li>1</li>
* <li>6:00</li>
* <li>/8:00</li>
* </ul>
* </p>
* @param startTime
* @param timeLabel
* @return
*/
private static long calculateTriggerTime(long startTime, String timeLabel) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(startTime);
if (timeLabel != null && timeLabel.contains(":")) {
// A. 具体时间点事件提前1小时
return startTime - (60 * 60 * 1000);
} else if ("全天".equals(timeLabel)) {
// B. 全天事件:当天早上 6:00
cal.set(Calendar.HOUR_OF_DAY, 6);
cal.set(Calendar.MINUTE, 0);
return cal.getTimeInMillis();
} else {
// C. 跨度/周期事件:当天早上 8:00
cal.set(Calendar.HOUR_OF_DAY, 8);
cal.set(Calendar.MINUTE, 0);
return cal.getTimeInMillis();
}
}
/**
*
* <p>
* Android
* <ul>
* <li>Android S使setExactAndAllowWhileIdle使setAndAllowWhileIdle</li>
* <li>Android S使setExactAndAllowWhileIdle</li>
* </ul>
* </p>
* @param context
* @param am AlarmManager
* @param id ID
* @param title
* @param time
*/
private static void setExactAlarm(Context context, AlarmManager am, long id, String title, long time) {
try {
Intent intent = new Intent(context, AiReminderReceiver.class);
intent.setAction(ACTION_AI_REMINDER);
intent.putExtra("note_id", id);
intent.putExtra("title", title);
PendingIntent pi = PendingIntent.getBroadcast(context, (int)id, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
// 核心修复:增加权限判断
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
if (am.canScheduleExactAlarms()) {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
} else {
// 退而求其次,使用非精准闹钟,防止崩溃
am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
}
} else {
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
}
} catch (Exception e) {
android.util.Log.e("AiReminder", "Failed to set alarm", e);
}
}
}

@ -1,82 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool.ai;
import com.google.gson.JsonObject;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.POST;
import retrofit2.http.Query;
/**
* Coze API
* <p>
* Coze API
* 使 Retrofit HTTP
* </p>
* @since 1.0
*/
public interface CozeApiService {
/**
*
* <p>
* Coze API AI
* </p>
* @param token "Bearer {token}"
* @param request
* @return Call<CozeResponse> Call
*/
@POST("v3/chat")
Call<CozeResponse> chat(@Header("Authorization") String token, @Body CozeRequest request);
/**
*
* <p>
*
* </p>
* @param token "Bearer {token}"
* @param chatId ID
* @param conversationId ID
* @return Call<CozeResponse> Call
*/
@POST("v3/chat/retrieve")
Call<CozeResponse> retrieveChat(
@Header("Authorization") String token,
@Query("chat_id") String chatId,
@Query("conversation_id") String conversationId
);
/**
*
* <p>
*
* </p>
* @param token "Bearer {token}"
* @param chatId ID
* @param conversationId ID
* @return Call<JsonObject> Call
*/
@GET("v3/chat/message/list")
Call<com.google.gson.JsonObject> getMessageList(
@Header("Authorization") String token,
@Query("chat_id") String chatId,
@Query("conversation_id") String conversationId
);
}

@ -1,101 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool.ai;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* Coze API
* <p>
* Coze API API
* API
* </p>
* @since 1.0
*/
public class CozeClient {
/**
* Coze API URL
*/
private static final String BASE_URL = "https://api.coze.cn/";
/**
* API
* <p>
* "Bearer {token}" Coze API
* </p>
* <p>
* TODO: 访 (PAT)
* </p>
*/
private static final String API_TOKEN = "Bearer pat_U2WS2ta9XWxz08ePQNloPw3mHxhmIGDf22ezG5JgyVC3CGrggE4SITQC4jZuIWE8";
/**
* ID
* <p>
* 使 Coze
* </p>
* <p>
* TODO: ID (Bot ID)
* </p>
*/
public static final String BOT_ID = "7601104751551660047";
/**
* Coze API
*/
private static CozeApiService apiService;
/**
* Coze API
* <p>
*
* 30 60
* </p>
* @return CozeApiService
*/
public static CozeApiService getInstance() {
if (apiService == null) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
apiService = retrofit.create(CozeApiService.class);
}
return apiService;
}
/**
*
* <p>
* Coze API "Bearer {token}"
* </p>
* @return
*/
public static String getAuthToken() {
return API_TOKEN;
}
}

@ -1,102 +0,0 @@
/**
* Coze API
* <p>
* Coze API ID ID
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
public class CozeRequest {
/**
* ID
*/
@SerializedName("bot_id")
private String botId;
/**
* ID
*/
@SerializedName("user_id")
private String userId;
/**
*
*/
@SerializedName("additional_messages")
private List<Message> additionalMessages;
/**
* 使
* <p>
* false使便
* </p>
*/
@SerializedName("stream")
private boolean stream = false;
/**
* CozeRequest
* <p>
*
* </p>
* @param botId ID
* @param userId ID
* @param content
*/
public CozeRequest(String botId, String userId, String content) {
this.botId = botId;
this.userId = userId;
this.additionalMessages = new ArrayList<>();
// 将指令和数据组合成一条用户消息发送给智能体
this.additionalMessages.add(new Message("user", content, "text"));
}
/**
*
* <p>
*
* </p>
*/
public static class Message {
/**
*
* <p>
* "user" "assistant"
* </p>
*/
@SerializedName("role")
private String role;
/**
*
*/
@SerializedName("content")
private String content;
/**
*
* <p>
* "text"
* </p>
*/
@SerializedName("content_type")
private String contentType;
/**
* Message
* @param role
* @param content
* @param contentType
*/
public Message(String role, String content, String contentType) {
this.role = role;
this.content = content;
this.contentType = contentType;
}
}
}

@ -1,66 +0,0 @@
/**
* Coze API
* <p>
* Coze API
* </p>
* @since 1.0
*/
package net.micode.notes.tool.ai;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class CozeResponse {
/**
*
* <p>
* 0 0
* </p>
*/
@SerializedName("code")
public int code;
/**
*
* <p>
*
* </p>
*/
@SerializedName("msg")
public String msg;
/**
*
*/
@SerializedName("data")
public ChatData data;
/**
*
* <p>
* ID ID
* </p>
*/
public static class ChatData {
/**
* ID
*/
public String id; // chat_id
/**
* ID
* <p>
* []
* </p>
*/
public String conversation_id;
/**
*
* <p>
* "completed"
* </p>
*/
public String status;
}
}

@ -1,185 +0,0 @@
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
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.Collections;
import java.util.List;
/**
*
* RecyclerView
*/
public class AgendaAdapter extends RecyclerView.Adapter<AgendaAdapter.AgendaViewHolder> {
/**
*
*/
private Context mContext;
/**
*
*/
private List<AgendaItem> mData = new ArrayList<>();
/**
*
*/
private OnAgendaActionListener mListener;
/**
*
*/
public interface OnAgendaActionListener {
/**
*
* @param item
*/
void onToggleComplete(AgendaItem item);
/**
*
* @param item
*/
void onDelete(AgendaItem item);
}
/**
*
* @param listener
*/
public void setOnAgendaActionListener(OnAgendaActionListener listener) {
this.mListener = listener;
}
/**
*
*/
public static class AgendaItem {
/**
* ID
*/
public long id;
/**
*
*/
public String title;
/**
*
*/
public String timeLabel;
/**
*
*/
public long startTime;
/**
*
*/
public boolean isCompleted;
/**
*
* @return
*/
public boolean isSpecificTime() {
return timeLabel != null && timeLabel.contains(":");
}
}
/**
*
* @param context
*/
public AgendaAdapter(Context context) {
this.mContext = context;
}
/**
*
* @param newList
*/
public void updateData(List<AgendaItem> newList) {
Collections.sort(newList, (a, b) -> {
if (a.isCompleted != b.isCompleted) return a.isCompleted ? 1 : -1;
if (a.isSpecificTime() != b.isSpecificTime()) return a.isSpecificTime() ? -1 : 1;
return Long.compare(a.startTime, b.startTime);
});
this.mData = newList;
notifyDataSetChanged();
}
/**
* ViewHolder
* @param parent
* @param viewType
* @return AgendaViewHolder
*/
@NonNull
@Override
public AgendaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.agenda_item, parent, false);
return new AgendaViewHolder(view);
}
/**
* ViewHolder
* @param holder ViewHolder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull AgendaViewHolder holder, int position) {
AgendaItem item = mData.get(position);
holder.tvContent.setText(item.title);
holder.tvTime.setText(item.timeLabel);
if (item.isCompleted) {
holder.ivCheck.setImageResource(R.drawable.checkbox_checked);
holder.tvContent.setPaintFlags(holder.tvContent.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
holder.tvContent.setTextColor(Color.LTGRAY);
} else {
holder.ivCheck.setImageResource(R.drawable.checkbox_unchecked);
holder.tvContent.setPaintFlags(holder.tvContent.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
holder.tvContent.setTextColor(Color.parseColor("#333333"));
}
holder.ivCheck.setOnClickListener(v -> { if (mListener != null) mListener.onToggleComplete(item); });
holder.btnDelete.setOnClickListener(v -> { if (mListener != null) mListener.onDelete(item); });
}
/**
*
* @return
*/
@Override
public int getItemCount() { return mData.size(); }
/**
* ViewHolder
*/
public static class AgendaViewHolder extends RecyclerView.ViewHolder {
/**
*
*/
ImageView ivCheck, btnDelete;
/**
*
*/
TextView tvTime, tvContent;
/**
*
* @param itemView
*/
public AgendaViewHolder(@NonNull View itemView) {
super(itemView);
ivCheck = itemView.findViewById(R.id.iv_agenda_check);
tvTime = itemView.findViewById(R.id.tv_agenda_time);
tvContent = itemView.findViewById(R.id.tv_agenda_content);
btnDelete = itemView.findViewById(R.id.btn_agenda_delete);
}
}
}

@ -1,278 +0,0 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CalendarView;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
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.data.Notes;
import net.micode.notes.tool.ai.AiReminderScheduler;
import net.micode.notes.data.Notes.NoteColumns;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
*
*
*/
public class AgendaFragment extends Fragment {
/**
* RecyclerView
*/
private RecyclerView mRecyclerView;
/**
*
*/
private AgendaAdapter mAdapter;
/**
*
*/
private CalendarView mCalendarView;
/**
*
*/
private TextView tvDateHeader;
/**
*
*/
private View emptyState;
/**
*
*/
private long mSelectedDayStart;
/**
*
*/
private long mSelectedDayEnd;
/**
*
*/
private boolean isCalendarExpanded = true;
/**
*
*/
private ImageButton btnToggleCalendar;
/**
*
*/
private int mQuickHour = -1;
/**
*
*/
private int mQuickMinute = -1;
/**
*
*/
private TextView tvQuickTime;
/**
*
* @param inflater Inflater
* @param container
* @param savedInstanceState
* @return
*/
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_agenda, container, false);
initUI(view);
return view;
}
/**
* UI
* @param view
*/
private void initUI(View view) {
mCalendarView = view.findViewById(R.id.calendar_view);
mRecyclerView = view.findViewById(R.id.agenda_list);
tvDateHeader = view.findViewById(R.id.tv_agenda_date_header);
emptyState = view.findViewById(R.id.ll_empty_state);
btnToggleCalendar = view.findViewById(R.id.btn_toggle_calendar);
// 日历折叠逻辑
btnToggleCalendar.setOnClickListener(v -> {
if (isCalendarExpanded) {
// 执行收起
mCalendarView.setVisibility(View.GONE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_down_float);
isCalendarExpanded = false;
} else {
// 执行展开
mCalendarView.setVisibility(View.VISIBLE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_up_float);
isCalendarExpanded = true;
}
});
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mAdapter = new AgendaAdapter(getContext());
mRecyclerView.setAdapter(mAdapter);
mCalendarView.setOnDateChangeListener((v, y, m, d) -> {
updateDateDisplay(y, m, d);
loadAgendaData();
// 体验优化:点击日期后,如果日历是展开的,则自动收起
if (isCalendarExpanded) {
mCalendarView.setVisibility(View.GONE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_down_float);
isCalendarExpanded = false;
}
});
// 设置监听器
mAdapter.setOnAgendaActionListener(new AgendaAdapter.OnAgendaActionListener() {
@Override
public void onToggleComplete(AgendaAdapter.AgendaItem item) {
ContentValues values = new ContentValues();
values.put(NoteColumns.IS_COMPLETED, item.isCompleted ? 0 : 1);
getContext().getContentResolver().update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[]{String.valueOf(item.id)});
loadAgendaData();
}
@Override
public void onDelete(AgendaAdapter.AgendaItem item) {
new AlertDialog.Builder(getContext())
.setTitle("确认删除")
.setMessage("删除此日程?")
.setPositiveButton("删除", (d, w) -> {
getContext().getContentResolver().delete(Notes.CONTENT_NOTE_URI, NoteColumns.ID + "=?", new String[]{String.valueOf(item.id)});
loadAgendaData();
}).setNegativeButton("取消", null).show();
}
});
tvQuickTime = view.findViewById(R.id.tv_set_quick_time);
// 点击“全天”弹出时间选择器
tvQuickTime.setOnClickListener(v -> {
Calendar now = Calendar.getInstance();
new android.app.TimePickerDialog(getContext(), (view1, hourOfDay, minute) -> {
mQuickHour = hourOfDay;
mQuickMinute = minute;
tvQuickTime.setText(String.format("%02d:%02d", hourOfDay, minute));
}, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), true).show();
});
// 修改 btn_quick_add 的点击逻辑
view.findViewById(R.id.btn_quick_add).setOnClickListener(v -> {
EditText et = view.findViewById(R.id.et_quick_add);
String title = et.getText().toString().trim();
if (TextUtils.isEmpty(title)) return;
ContentValues cv = new ContentValues();
cv.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
cv.put(NoteColumns.IS_AGENDA, 1);
cv.put(NoteColumns.SNIPPET, title);
// 计算具体时间
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mSelectedDayStart); // 基于日历选中的那一天
if (mQuickHour != -1) {
cal.set(Calendar.HOUR_OF_DAY, mQuickHour);
cal.set(Calendar.MINUTE, mQuickMinute);
cv.put(NoteColumns.TIME_LABEL, String.format("%02d:%02d", mQuickHour, mQuickMinute));
cv.put(NoteColumns.AGENDA_DATE, cal.getTimeInMillis());
cv.put(NoteColumns.AGENDA_END_DATE, cal.getTimeInMillis()); // 点事件
} else {
cv.put(NoteColumns.TIME_LABEL, "全天");
cv.put(NoteColumns.AGENDA_DATE, mSelectedDayStart);
cv.put(NoteColumns.AGENDA_END_DATE, mSelectedDayEnd);
}
cv.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
cv.put(NoteColumns.LOCAL_MODIFIED, 1);
getContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, cv);
// 核心补丁:让 AI 助理感知到新日程
try {
AiReminderScheduler.updateAllReminders(getContext());
android.util.Log.d("AiReminder", "Scheduled from Agenda Quick Add");
} catch (Exception e) {
e.printStackTrace();
}
// 重置 UI
et.setText("");
tvQuickTime.setText("全天");
mQuickHour = -1;
mQuickMinute = -1;
loadAgendaData();
});
mCalendarView.setOnDateChangeListener((v, y, m, d) -> {
updateDateDisplay(y, m, d);
loadAgendaData();
});
updateDateDisplay(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
loadAgendaData();
}
/**
*
* @param year
* @param month
* @param day
*/
private void updateDateDisplay(int year, int month, int day) {
tvDateHeader.setText(String.format("%d年%d月%d日 的日程", year, month + 1, day));
Calendar cal = Calendar.getInstance();
cal.set(year, month, day, 0, 0, 0);
mSelectedDayStart = cal.getTimeInMillis();
cal.set(year, month, day, 23, 59, 59);
mSelectedDayEnd = cal.getTimeInMillis();
}
/**
*
*/
private void loadAgendaData() {
if (getContext() == null) return;
Cursor cursor = getContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID, NoteColumns.SNIPPET, NoteColumns.TIME_LABEL, NoteColumns.AGENDA_DATE, NoteColumns.IS_COMPLETED},
NoteColumns.IS_AGENDA + "=1 AND " + NoteColumns.AGENDA_DATE + "<=? AND " + NoteColumns.AGENDA_END_DATE + ">=?",
new String[]{String.valueOf(mSelectedDayEnd), String.valueOf(mSelectedDayStart)},
null);
List<AgendaAdapter.AgendaItem> list = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
AgendaAdapter.AgendaItem item = new AgendaAdapter.AgendaItem();
item.id = cursor.getLong(0);
item.title = cursor.getString(1);
item.timeLabel = cursor.getString(2);
item.startTime = cursor.getLong(3);
item.isCompleted = cursor.getInt(4) == 1;
list.add(item);
}
cursor.close();
}
mAdapter.updateData(list);
// 使用 emptyState 控制显示
if (emptyState != null) {
emptyState.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
}
}
}

@ -40,32 +40,12 @@ import net.micode.notes.tool.DataUtils;
import java.io.IOException;
/**
*
* 便
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
/**
* 便ID
*/
private long mNoteId;
/**
* 便
*/
private String mSnippet;
/**
*
*/
private static final int SNIPPET_PREW_MAX_LEN = 60;
/**
*
*/
MediaPlayer mPlayer;
/**
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -103,18 +83,11 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
}
/**
*
* @return
*/
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
/**
*
*/
private void playAlarmSound() {
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
@ -132,19 +105,20 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
mPlayer.setLooping(true);
mPlayer.start();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
*
*/
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
@ -156,11 +130,6 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
dialog.show().setOnDismissListener(this);
}
/**
*
* @param dialog
* @param which
*/
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
@ -174,18 +143,11 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
}
}
/**
*
* @param dialog
*/
public void onDismiss(DialogInterface dialog) {
stopAlarmSound();
finish();
}
/**
*
*/
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop();

@ -28,34 +28,16 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* 便
*/
public class AlarmInitReceiver extends BroadcastReceiver {
/**
*
*/
private static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
/**
* ID
*/
private static final int COLUMN_ID = 0;
/**
*
*/
private static final int COLUMN_ALERTED_DATE = 1;
/**
* 广
* @param context
* @param intent
*/
@Override
public void onReceive(Context context, Intent intent) {
long currentDate = System.currentTimeMillis();

@ -20,16 +20,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
*
* 广
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
* 广
* @param context
* @param intent
*/
@Override
public void onReceive(Context context, Intent intent) {
intent.setClass(context, AlarmAlertActivity.class);

@ -1,152 +0,0 @@
package net.micode.notes.ui;
import android.graphics.Typeface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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.List;
/**
*
* RecyclerView
*/
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder> {
/**
*
*/
private List<ChatMessage> mMessages;
/**
*
*/
private java.text.SimpleDateFormat timeFormat = new java.text.SimpleDateFormat("MM-dd HH:mm", java.util.Locale.CHINA);
/**
*
* @param messages
*/
public ChatAdapter(List<ChatMessage> messages) {
this.mMessages = messages;
}
/**
*
* @param position
* @return 0-1-AI2-
*/
@Override
public int getItemViewType(int position) {
ChatMessage msg = mMessages.get(position);
if (msg.senderType == 0) return 0; // 用户消息
if (msg.msgType == 1) return 2; // 提醒卡片
return 1; // AI 普通回复
}
/**
* ViewHolder
* @param parent
* @param viewType
* @return ChatViewHolder
*/
@NonNull
@Override
public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_msg, parent, false);
return new ChatViewHolder(view);
}
/**
* ViewHolder
* @param holder ViewHolder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) {
ChatMessage msg = mMessages.get(position);
// 处理时间显示
holder.tvTime.setVisibility(View.VISIBLE);
holder.tvTime.setText(timeFormat.format(new java.util.Date(msg.createdAt)));
// 如果想要更像微信(时间太近就不显示),可以加这个逻辑:
if (position > 0) {
ChatMessage preMsg = mMessages.get(position - 1);
if (msg.createdAt - preMsg.createdAt < 5 * 60 * 1000) { // 5分钟内
holder.tvTime.setVisibility(View.GONE);
}
}
int viewType = getItemViewType(position);
// 隐藏所有,根据类型显示
holder.leftLayout.setVisibility(View.GONE);
holder.rightLayout.setVisibility(View.GONE);
if (viewType == 0) { // 用户
holder.rightLayout.setVisibility(View.VISIBLE);
holder.tvRight.setText(msg.content);
} else { // AI 或 提醒
holder.leftLayout.setVisibility(View.VISIBLE);
holder.tvLeft.setText(msg.content);
if (viewType == 2) { // 提醒特殊样式
holder.tvLeft.setBackgroundResource(R.drawable.bg_bubble_reminder);
holder.tvLeft.setTypeface(null, Typeface.BOLD);
holder.tvLeft.setText("? 日程提醒:\n" + msg.content);
} else {
holder.tvLeft.setBackgroundResource(R.drawable.bg_bubble_ai);
holder.tvLeft.setTypeface(null, Typeface.NORMAL);
}
}
}
/**
*
* @return
*/
@Override
public int getItemCount() { return mMessages.size(); }
/**
* ViewHolder
*/
static class ChatViewHolder extends RecyclerView.ViewHolder {
/**
* AI
*/
View leftLayout;
/**
*
*/
View rightLayout;
/**
* AI
*/
TextView tvLeft;
/**
*
*/
TextView tvRight;
/**
*
*/
TextView tvTime;
/**
*
* @param v
*/
ChatViewHolder(View v) {
super(v);
leftLayout = v.findViewById(R.id.ll_left_layout);
rightLayout = v.findViewById(R.id.ll_right_layout);
tvLeft = v.findViewById(R.id.tv_msg_left);
tvRight = v.findViewById(R.id.tv_msg_right);
tvTime = v.findViewById(R.id.tv_chat_time); // 绑定
}
}
}

@ -1,256 +0,0 @@
package net.micode.notes.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.model.ChatMessage;
import java.util.ArrayList;
import java.util.List;
/**
*
* AI
*/
public class ChatFragment extends Fragment {
/**
* RecyclerView
*/
private RecyclerView mRecyclerView;
/**
*
*/
private ChatAdapter mAdapter;
/**
*
*/
private List<ChatMessage> mMessages = new ArrayList<>();
/**
*
*/
private EditText mInput;
/**
*
*/
private android.database.ContentObserver mChatObserver;
/**
* URI
*/
private static final android.net.Uri CHAT_URI = android.net.Uri.parse("content://micode_notes/chat_messages");
/**
*
* @param inflater Inflater
* @param container
* @param savedInstanceState
* @return
*/
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_chat, container, false);
mRecyclerView = view.findViewById(R.id.rv_chat_list);
mInput = view.findViewById(R.id.et_chat_input);
mAdapter = new ChatAdapter(mMessages);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setAdapter(mAdapter);
view.findViewById(R.id.btn_chat_send).setOnClickListener(v -> {
final String text = mInput.getText().toString().trim();
if (text.isEmpty()) return;
// ============================================================
// 【手术点 1修改标题为“正在输入...”】
// ============================================================
if (getActivity() instanceof androidx.appcompat.app.AppCompatActivity) {
androidx.appcompat.app.ActionBar actionBar = ((androidx.appcompat.app.AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle("对方正在输入中......");
}
}
// --- 第一部分:将用户消息存入数据库 (保持原逻辑) ---
new Thread(() -> {
android.content.ContentValues userValues = new android.content.ContentValues();
userValues.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 0);
userValues.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0);
userValues.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, text);
userValues.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
getContext().getContentResolver().insert(CHAT_URI, userValues);
}).start();
mInput.setText("");
// --- 第二部分:向 AI 发起请求 ---
new Thread(() -> {
try {
com.google.gson.JsonObject json = new com.google.gson.JsonObject();
json.addProperty("intent", "chat");
json.addProperty("payload", text);
json.addProperty("current_time", net.micode.notes.tool.ai.AiDataSyncHelper.getCurrentTime());
net.micode.notes.tool.ai.CozeRequest request = new net.micode.notes.tool.ai.CozeRequest(
net.micode.notes.tool.ai.CozeClient.BOT_ID, "user_demo", json.toString());
retrofit2.Response<net.micode.notes.tool.ai.CozeResponse> response =
net.micode.notes.tool.ai.CozeClient.getInstance().chat(
net.micode.notes.tool.ai.CozeClient.getAuthToken(), request).execute();
if (response.isSuccessful() && response.body() != null && response.body().data != null) {
String chatId = response.body().data.id;
String convId = response.body().data.conversation_id;
String status = "";
int retries = 0;
while (!"completed".equals(status) && retries < 30) {
Thread.sleep(2000);
retrofit2.Response<net.micode.notes.tool.ai.CozeResponse> poll =
net.micode.notes.tool.ai.CozeClient.getInstance().retrieveChat(
net.micode.notes.tool.ai.CozeClient.getAuthToken(), chatId, convId).execute();
if (poll.isSuccessful() && poll.body() != null && poll.body().data != null) {
status = poll.body().data.status;
if ("failed".equals(status)) break;
}
retries++;
}
if ("completed".equals(status)) {
retrofit2.Response<com.google.gson.JsonObject> msgList =
net.micode.notes.tool.ai.CozeClient.getInstance().getMessageList(
net.micode.notes.tool.ai.CozeClient.getAuthToken(), chatId, convId).execute();
if (msgList.isSuccessful() && msgList.body() != null) {
com.google.gson.JsonArray dataArray = msgList.body().getAsJsonArray("data");
for (com.google.gson.JsonElement el : dataArray) {
com.google.gson.JsonObject m = el.getAsJsonObject();
if ("assistant".equals(m.get("role").getAsString()) &&
"answer".equals(m.get("type").getAsString())) {
String rawAnswer = m.get("content").getAsString();
// ============================================================
// 【手术点 2JSON 解析脱壳逻辑】
// ============================================================
String finalAnswer = rawAnswer;
if (rawAnswer != null && rawAnswer.trim().startsWith("{")) {
try {
com.google.gson.JsonObject jo = com.google.gson.JsonParser.parseString(rawAnswer).getAsJsonObject();
if (jo.has("content")) {
finalAnswer = jo.get("content").getAsString();
}
} catch (Exception e) {
// 解析失败则保持 rawAnswer 原样
}
}
// 存入数据库
android.content.ContentValues aiValues = new android.content.ContentValues();
aiValues.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 1);
aiValues.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0);
aiValues.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, finalAnswer);
aiValues.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis());
getContext().getContentResolver().insert(CHAT_URI, aiValues);
break;
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// ============================================================
// 【手术点 3请求结束恢复标题为“AI 助理”】
// ============================================================
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (getActivity() instanceof androidx.appcompat.app.AppCompatActivity) {
androidx.appcompat.app.ActionBar actionBar = ((androidx.appcompat.app.AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle("AI 助理");
}
}
});
}
}
}).start();
});
// 1. 初次加载历史记录
loadHistory();
// 2. 注册观察者:监听数据库变化
mChatObserver = new android.database.ContentObserver(new android.os.Handler()) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
android.util.Log.d("ChatFragment", "Database changed, reloading...");
loadHistory(); // 数据库一变就重新加载
}
};
getContext().getContentResolver().registerContentObserver(CHAT_URI, true, mChatObserver);
return view;
}
/**
*
*/
@Override
public void onDestroyView() {
super.onDestroyView();
if (mChatObserver != null) {
getContext().getContentResolver().unregisterContentObserver(mChatObserver);
}
}
/**
*
*/
private void loadHistory() {
new Thread(() -> {
android.database.Cursor cursor = getContext().getContentResolver().query(
CHAT_URI,
null, null, null, "created_at ASC" // 按时间顺序排列
);
List<ChatMessage> history = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
// 根据 Step 1 定义的列顺序取值
int senderType = cursor.getInt(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE));
int msgType = cursor.getInt(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE));
String content = cursor.getString(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.CONTENT));
long time = cursor.getLong(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.CREATED_AT));
ChatMessage msg = new ChatMessage(senderType, msgType, content);
msg.createdAt = time;
history.add(msg);
}
cursor.close();
}
// 回到主线程更新 UI
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
mMessages.clear();
mMessages.addAll(history);
mAdapter.notifyDataSetChanged();
if (mMessages.size() > 0) {
mRecyclerView.scrollToPosition(mMessages.size() - 1);
}
});
}
}).start();
}
}

@ -28,119 +28,40 @@ import android.view.View;
import android.widget.FrameLayout;
import android.widget.NumberPicker;
/**
*
*
*/
public class DateTimePicker extends FrameLayout {
/**
*
*/
private static final boolean DEFAULT_ENABLE_STATE = true;
/**
*
*/
private static final int HOURS_IN_HALF_DAY = 12;
/**
*
*/
private static final int HOURS_IN_ALL_DAY = 24;
/**
*
*/
private static final int DAYS_IN_ALL_WEEK = 7;
/**
*
*/
private static final int DATE_SPINNER_MIN_VAL = 0;
/**
*
*/
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
/**
* 24
*/
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
/**
* 24
*/
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
/**
* 12
*/
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
/**
* 12
*/
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
/**
*
*/
private static final int MINUT_SPINNER_MIN_VAL = 0;
/**
*
*/
private static final int MINUT_SPINNER_MAX_VAL = 59;
/**
* /
*/
private static final int AMPM_SPINNER_MIN_VAL = 0;
/**
* /
*/
private static final int AMPM_SPINNER_MAX_VAL = 1;
/**
*
*/
private final NumberPicker mDateSpinner;
/**
*
*/
private final NumberPicker mHourSpinner;
/**
*
*/
private final NumberPicker mMinuteSpinner;
/**
* /
*/
private final NumberPicker mAmPmSpinner;
/**
*
*/
private Calendar mDate;
/**
*
*/
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
/**
*
*/
private boolean mIsAm;
/**
* 24
*/
private boolean mIs24HourView;
/**
*
*/
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
/**
*
*/
private boolean mInitialising;
/**
*
*/
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@ -237,46 +158,19 @@ public class DateTimePicker extends FrameLayout {
}
};
/**
*
*/
public interface OnDateTimeChangedListener {
/**
*
* @param view
* @param year
* @param month
* @param dayOfMonth
* @param hourOfDay
* @param minute
*/
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
/**
*
* @param context
*/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
/**
*
* @param context
* @param date
*/
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
/**
*
* @param context
* @param date
* @param is24HourView 24
*/
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance();
@ -454,10 +348,6 @@ public class DateTimePicker extends FrameLayout {
return mDate.get(Calendar.HOUR_OF_DAY);
}
/**
*
* @return
*/
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
@ -544,9 +434,6 @@ public class DateTimePicker extends FrameLayout {
updateAmPmControl();
}
/**
*
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
@ -561,9 +448,6 @@ public class DateTimePicker extends FrameLayout {
mDateSpinner.invalidate();
}
/**
* /
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
@ -574,9 +458,6 @@ public class DateTimePicker extends FrameLayout {
}
}
/**
*
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
@ -595,9 +476,6 @@ public class DateTimePicker extends FrameLayout {
mOnDateTimeChangedListener = callback;
}
/**
*
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),

@ -29,46 +29,17 @@ import android.content.DialogInterface.OnClickListener;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
*
*
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
/**
*
*/
private Calendar mDate = Calendar.getInstance();
/**
* 24
*/
private boolean mIs24HourView;
/**
*
*/
private OnDateTimeSetListener mOnDateTimeSetListener;
/**
*
*/
private DateTimePicker mDateTimePicker;
/**
*
*/
public interface OnDateTimeSetListener {
/**
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
mDateTimePicker = new DateTimePicker(context);
@ -93,26 +64,14 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener
updateTitle(mDate.getTimeInMillis());
}
/**
* 24
* @param is24HourView 24
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
* @param callBack
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
* @param date
*/
private void updateTitle(long date) {
int flag =
DateUtils.FORMAT_SHOW_YEAR |
@ -122,11 +81,6 @@ public class DateTimePickerDialog extends AlertDialog implements OnClickListener
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
*
* @param arg0
* @param arg1
*/
public void onClick(DialogInterface arg0, int arg1) {
if (mOnDateTimeSetListener != null) {
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());

@ -27,30 +27,11 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import net.micode.notes.R;
/**
*
*
*/
public class DropdownMenu {
/**
*
*/
private Button mButton;
/**
*
*/
private PopupMenu mPopupMenu;
/**
*
*/
private Menu mMenu;
/**
*
* @param context
* @param button
* @param menuId ID
*/
public DropdownMenu(Context context, Button button, int menuId) {
mButton = button;
mButton.setBackgroundResource(R.drawable.dropdown_icon);
@ -64,29 +45,16 @@ public class DropdownMenu {
});
}
/**
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
if (mPopupMenu != null) {
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
* ID
* @param id ID
* @return
*/
public MenuItem findItem(int id) {
return mMenu.findItem(id);
}
/**
*
* @param title
*/
public void setTitle(CharSequence title) {
mButton.setText(title);
}

@ -1,139 +0,0 @@
package net.micode.notes.ui;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
/**
*
*
*/
public class FloatingService extends Service {
/**
*
*/
private WindowManager windowManager;
/**
*
*/
private View floatingView;
/**
*
*/
private WindowManager.LayoutParams params;
/**
*
* @param intent
* @return null
*/
@Override
public IBinder onBind(Intent intent) { return null; }
/**
*
*/
@Override
public void onCreate() {
super.onCreate();
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null);
// 设置布局参数
int layoutFlag;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutFlag = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutFlag = WindowManager.LayoutParams.TYPE_PHONE;
}
params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
layoutFlag,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 保证外部可点击
PixelFormat.TRANSLUCENT
);
params.gravity = Gravity.TOP | Gravity.START;
params.x = 100;
params.y = 100;
// 挂载到窗口
windowManager.addView(floatingView, params);
// 设置交互逻辑
setupInteraction();
}
/**
*
*/
private void setupInteraction() {
View ball = floatingView.findViewById(R.id.iv_floating_ball);
ball.setOnTouchListener(new View.OnTouchListener() {
private int initialX, initialY;
private float initialTouchX, initialTouchY;
private long touchStartTime;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
touchStartTime = System.currentTimeMillis();
return true;
case MotionEvent.ACTION_MOVE:
params.x = initialX + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
windowManager.updateViewLayout(floatingView, params);
return true;
case MotionEvent.ACTION_UP:
// 如果按下时间短且移动距离小,判定为点击
if (System.currentTimeMillis() - touchStartTime < 200) {
openNewNote();
}
return true;
}
return false;
}
});
}
/**
*
*/
private void openNewNote() {
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, Notes.ID_ROOT_FOLDER);
// [核心] Service 调起 Activity 必须加此 Flag
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
/**
*
*/
@Override
public void onDestroy() {
super.onDestroy();
if (floatingView != null) windowManager.removeView(floatingView);
}
}

@ -28,56 +28,26 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* CursorAdapter
*/
public class FoldersListAdapter extends CursorAdapter {
/**
*
*/
public static final String [] PROJECTION = {
NoteColumns.ID,
NoteColumns.SNIPPET
};
/**
* ID
*/
public static final int ID_COLUMN = 0;
/**
*
*/
public static final int NAME_COLUMN = 1;
/**
*
* @param context
* @param c
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
// TODO Auto-generated constructor stub
}
/**
*
* @param context
* @param cursor
* @param parent
* @return
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new FolderListItem(context);
}
/**
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof FolderListItem) {
@ -87,41 +57,21 @@ public class FoldersListAdapter extends CursorAdapter {
}
}
/**
*
* @param context
* @param position
* @return
*/
public String getFolderName(Context context, int position) {
Cursor cursor = (Cursor) getItem(position);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
/**
*
*/
private class FolderListItem extends LinearLayout {
/**
*
*/
private TextView mName;
/**
*
* @param context
*/
public FolderListItem(Context context) {
super(context);
inflate(context, R.layout.folder_list_item, this);
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
*
* @param name
*/
public void bind(String name) {
mName.setText(name);
}

@ -1,160 +0,0 @@
package net.micode.notes.ui;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Toast;
import com.google.firebase.auth.FirebaseUser;
import android.content.ContentValues;
import android.net.Uri;
import net.micode.notes.data.Notes;
import androidx.appcompat.app.AppCompatActivity;
import com.google.firebase.auth.FirebaseAuth;
import net.micode.notes.R;
/**
*
*
*/
public class LoginActivity extends AppCompatActivity {
/**
*
*/
private EditText mEmailField;
/**
*
*/
private EditText mPasswordField;
/**
*
*/
private Button mLoginBtn;
/**
*
*/
private ProgressBar mLoadingBar;
/**
* Firebase
*/
private FirebaseAuth mAuth;
/**
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 1. 初始化 Firebase Auth
mAuth = FirebaseAuth.getInstance();
// 2. 绑定 UI 控件
mEmailField = findViewById(R.id.et_email);
mPasswordField = findViewById(R.id.et_password);
mLoginBtn = findViewById(R.id.btn_login);
mLoadingBar = findViewById(R.id.loading_bar);
// 3. 登录逻辑
mLoginBtn.setOnClickListener(v -> handleAuth());
}
/**
*
*/
@Override
protected void onStart() {
super.onStart();
// [新增] 自动登录检查:如果 Firebase 已经记录了登录状态,直接跳过登录页
FirebaseUser currentUser = mAuth.getCurrentUser();
if (currentUser != null) {
updateLocalAccount(currentUser); // 确保本地存储了最新 UID
startActivity(new Intent(this, NotesListActivity.class));
finish();
}
}
/**
*
* @param user Firebase
*/
private void updateLocalAccount(FirebaseUser user) {
android.content.ContentValues values = new android.content.ContentValues();
values.put(net.micode.notes.data.Notes.AccountColumns.UID, user.getUid());
values.put(net.micode.notes.data.Notes.AccountColumns.EMAIL, user.getEmail());
// 使用本地 ContentResolver 写入 user_account 表
getContentResolver().insert(
android.net.Uri.parse("content://" + net.micode.notes.data.Notes.AUTHORITY + "/user_account"),
values
);
}
/**
*
*/
private void handleAuth() {
String email = mEmailField.getText().toString().trim();
String password = mPasswordField.getText().toString().trim();
if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password)) {
Toast.makeText(this, "请填写邮箱和密码", Toast.LENGTH_SHORT).show();
return;
}
if (password.length() < 6) {
Toast.makeText(this, "密码至少需要6位", Toast.LENGTH_SHORT).show();
return;
}
showLoading(true);
// 先尝试登录
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
onAuthSuccess();
} else {
// 登录失败,尝试注册(方便演示)
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(this, regTask -> {
showLoading(false);
if (regTask.isSuccessful()) {
onAuthSuccess();
} else {
Toast.makeText(this, "认证失败: " + regTask.getException().getMessage(), Toast.LENGTH_LONG).show();
}
});
}
});
}
/**
*
*/
private void onAuthSuccess() {
// [新增] 成功认证后,立即同步身份到本地
FirebaseUser user = mAuth.getCurrentUser();
updateLocalAccount(user);
Toast.makeText(this, "登录成功!", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, NotesListActivity.class));
finish();
}
/**
*
* @param loading
*/
private void showLoading(boolean loading) {
mLoadingBar.setVisibility(loading ? View.VISIBLE : View.GONE);
mLoginBtn.setEnabled(!loading);
}
}

@ -37,40 +37,15 @@ import net.micode.notes.R;
import java.util.HashMap;
import java.util.Map;
/**
* 便
* EditText便
*/
public class NoteEditText extends EditText {
/**
*
*/
private static final String TAG = "NoteEditText";
/**
*
*/
private int mIndex;
/**
*
*/
private int mSelectionStartBeforeDelete;
/**
*
*/
private static final String SCHEME_TEL = "tel:" ;
/**
* HTTP
*/
private static final String SCHEME_HTTP = "http:" ;
/**
*
*/
private static final String SCHEME_EMAIL = "mailto:" ;
/**
*
*/
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
@ -79,87 +54,51 @@ public class NoteEditText extends EditText {
}
/**
*
* {@link NoteEditActivity}
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
* {@link KeyEvent#KEYCODE_DEL}
* @param index
* @param text
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
void onEditTextDelete(int index, String text);
/**
* {@link KeyEvent#KEYCODE_ENTER}
* @param index
* @param text
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
void onEditTextEnter(int index, String text);
/**
*
* @param index
* @param hasText
* Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
/**
*
*/
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
*
* @param context
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
}
/**
*
* @param index
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
* @param listener
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
*
* @param context
* @param attrs
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
*
* @param context
* @param attrs
* @param defStyle
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
@ -182,12 +121,6 @@ public class NoteEditText extends EditText {
return super.onTouchEvent(event);
}
/**
*
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
@ -205,12 +138,6 @@ public class NoteEditText extends EditText {
return super.onKeyDown(keyCode, event);
}
/**
*
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
@ -240,12 +167,6 @@ public class NoteEditText extends EditText {
return super.onKeyUp(keyCode, event);
}
/**
*
* @param focused
* @param direction
* @param previouslyFocusedRect
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
@ -258,10 +179,6 @@ public class NoteEditText extends EditText {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
*
* @param menu
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
if (getText() instanceof Spanned) {

@ -26,16 +26,7 @@ import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
/**
*
* <p>
*
* 使
*/
public class NoteItemData {
/**
*
*/
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
@ -49,164 +40,42 @@ public class NoteItemData {
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.TITLE,
NoteColumns.IS_PINNED
};
/**
* ID
*/
private static final int ID_COLUMN = 0;
/**
*
*/
private static final int ALERTED_DATE_COLUMN = 1;
/**
* ID
*/
private static final int BG_COLOR_ID_COLUMN = 2;
/**
*
*/
private static final int CREATED_DATE_COLUMN = 3;
/**
*
*/
private static final int HAS_ATTACHMENT_COLUMN = 4;
/**
*
*/
private static final int MODIFIED_DATE_COLUMN = 5;
/**
*
*/
private static final int NOTES_COUNT_COLUMN = 6;
/**
* ID
*/
private static final int PARENT_ID_COLUMN = 7;
/**
*
*/
private static final int SNIPPET_COLUMN = 8;
/**
*
*/
private static final int TYPE_COLUMN = 9;
/**
* ID
*/
private static final int WIDGET_ID_COLUMN = 10;
/**
*
*/
private static final int WIDGET_TYPE_COLUMN = 11;
/**
*
*/
private static final int TITLE_COLUMN = 12;
/**
*
*/
private static final int IS_PINNED_COLUMN = 13;
/**
* ID
*/
private long mId;
/**
*
*/
private long mAlertDate;
/**
* ID
*/
private int mBgColorId;
/**
*
*/
private long mCreatedDate;
/**
*
*/
private boolean mHasAttachment;
/**
*
*/
private long mModifiedDate;
/**
*
*/
private int mNotesCount;
/**
* ID
*/
private long mParentId;
/**
*
*/
private String mSnippet;
/**
*
*/
private int mType;
/**
* ID
*/
private int mWidgetId;
/**
*
*/
private int mWidgetType;
/**
*
*/
private String mName;
/**
*
*/
private String mPhoneNumber;
/**
*
*/
private String mTitle;
/**
*
*/
private boolean mIsPinned;
/**
*
*/
private boolean mIsLastItem;
/**
*
*/
private boolean mIsFirstItem;
/**
*
*/
private boolean mIsOnlyOneItem;
/**
*
*/
private boolean mIsOneNoteFollowingFolder;
/**
*
*/
private boolean mIsMultiNotesFollowingFolder;
/**
*
* <p>
*
* ID
* ID
*
*
* @param context
* @param cursor
*/
public NoteItemData(Context context, Cursor cursor) {
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
@ -216,38 +85,12 @@ public class NoteItemData {
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// [新增修复] 读取数据库内容后,立即剥离 HTML 标签,只保留纯文本用于列表展示
String rawSnippet = cursor.getString(SNIPPET_COLUMN);
if (rawSnippet != null && rawSnippet.contains("<")) {
try {
// 将 HTML 转换为 Spanned再 toString() 即为纯文本
mSnippet = android.text.Html.fromHtml(rawSnippet).toString();
} catch (Exception e) {
mSnippet = rawSnippet;
}
} else {
mSnippet = rawSnippet;
}
// 原有的清洗逻辑(去除清单符号)可以保留在后面
if (mSnippet != null) {
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "")
.replace(NoteEditActivity.TAG_UNCHECKED, "")
.replace("?", "")
.replace("?", "")
.trim(); // 去除首尾空格和换行
}
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// 使用 getColumnIndex 更加安全
int titleColumnIndex = cursor.getColumnIndex(Notes.NoteColumns.TITLE);
if (titleColumnIndex != -1) {
mTitle = cursor.getString(titleColumnIndex);
} else {
mTitle = "";
}
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
@ -263,19 +106,9 @@ public class NoteItemData {
if (mName == null) {
mName = "";
}
// [新增] 读取数据库中的置顶状态 (注意checkPostion(cursor) 之前)
mIsPinned = cursor.getInt(IS_PINNED_COLUMN) > 0;
checkPostion(cursor);
}
/**
*
* <p>
*
*
*
* @param cursor
*/
private void checkPostion(Cursor cursor) {
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
@ -301,227 +134,90 @@ public class NoteItemData {
}
}
/**
*
*
* @return
*/
public boolean isPinned() {
return mIsPinned;
}
/**
*
*
* @return
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
*
*
* @return
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
*
* @return
*/
public boolean isLast() {
return mIsLastItem;
}
/**
*
*
* @return
*/
public String getCallName() {
return mName;
}
/**
*
*
* @return
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
*
* @return
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
/**
* ID
*
* @return ID
*/
public long getId() {
return mId;
}
/**
*
*
* @return
*/
public long getAlertDate() {
return mAlertDate;
}
/**
*
*
* @return
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
*
*
* @return
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
*
*
* @return
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* ID
*
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* ID
*
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
*
*
* @return
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* ID
* <p>
* ID
*
* @return ID
*/
public long getFolderId () {
return mParentId;
}
/**
*
*
* @return
*/
public int getType() {
return mType;
}
/**
*
*
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* ID
*
* @return ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
*
*
* @return
*/
public String getSnippet() {
return mSnippet;
}
/**
*
*
* @return
*/
public String getTitle() {
return mTitle;
}
/**
*
* <p>
* 0
*
* @return
*/
public boolean hasAlert() {
return (mAlertDate > 0);
}
/**
*
* <p>
* IDID
*
* @return
*/
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
/**
*
* <p>
*
*
* @param cursor
* @return
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}

@ -14,13 +14,6 @@
* limitations under the License.
*/
/**
* 便
* <p>
* 便
* </p>
* @since 1.0
*/
package net.micode.notes.ui;
import android.content.Context;
@ -39,53 +32,17 @@ import java.util.Iterator;
public class NotesListAdapter extends CursorAdapter {
/**
*
*/
private static final String TAG = "NotesListAdapter";
/**
*
*/
private Context mContext;
/**
*
*/
private HashMap<Integer, Boolean> mSelectedIndex;
/**
* 便
*/
private int mNotesCount;
/**
*
*/
private boolean mChoiceMode;
/**
*
* <p>
* ID
* </p>
*/
public static class AppWidgetAttribute {
/**
* ID
*/
public int widgetId;
/**
*
*/
public int widgetType;
};
/**
*
* @param context
*/
public NotesListAdapter(Context context) {
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>();
@ -93,24 +50,11 @@ public class NotesListAdapter extends CursorAdapter {
mNotesCount = 0;
}
/**
*
* @param context
* @param cursor
* @param parent
* @return
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context);
}
/**
*
* @param view
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
@ -120,37 +64,20 @@ public class NotesListAdapter extends CursorAdapter {
}
}
/**
*
* @param position
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
/**
*
* @return
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
*
* @param mode
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
}
/**
*
* @param checked
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
@ -162,10 +89,6 @@ public class NotesListAdapter extends CursorAdapter {
}
}
/**
* ID
* @return ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
@ -182,10 +105,6 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
/**
*
* @return
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
@ -209,10 +128,6 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
@ -228,20 +143,11 @@ public class NotesListAdapter extends CursorAdapter {
return count;
}
/**
*
* @return
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
* @param position
* @return
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
@ -249,28 +155,18 @@ public class NotesListAdapter extends CursorAdapter {
return mSelectedIndex.get(position);
}
/**
*
*/
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount();
}
/**
*
* @param cursor
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount();
}
/**
* 便
*/
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {

@ -30,74 +30,24 @@ import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
import android.text.TextUtils;
/**
* 便
* <p>
* 便
* </p>
* @since 1.0
*/
public class NotesListItem extends LinearLayout {
/**
*
*/
private ImageView mAlert;
/**
*
*/
private TextView mTitle;
/**
*
*/
private TextView mTime;
/**
* 便
*/
private TextView mNoteTitle;
/**
* 便
*/
private TextView mCallName;
private NoteItemData mItemData;
/**
*
*/
private CheckBox mCheckBox;
/**
*
*/
private ImageView mPin;
/**
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mNoteTitle = (TextView) findViewById(R.id.tv_note_title);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
// [新增] 初始化 View
mPin = (ImageView) findViewById(R.id.iv_pin_icon);
}
/**
*
* @param context
* @param data 便
* @param choiceMode
* @param checked
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
@ -106,25 +56,17 @@ public class NotesListItem extends LinearLayout {
mCheckBox.setVisibility(View.GONE);
}
// [新增] 绑定置顶状态
// 只有当类型是普通便签(TYPE_NOTE)且 isPinned 为 true 时才显示
if (data.getType() == Notes.TYPE_NOTE && data.isPinned()) {
mPin.setVisibility(View.VISIBLE);
} else {
mPin.setVisibility(View.GONE);
}
mItemData = data;
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mNoteTitle.setVisibility(View.GONE);
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mNoteTitle.setVisibility(View.VISIBLE);
mNoteTitle.setText(data.getCallName());
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
@ -134,7 +76,7 @@ public class NotesListItem extends LinearLayout {
mAlert.setVisibility(View.GONE);
}
} else {
mNoteTitle.setVisibility(View.VISIBLE);
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
@ -143,9 +85,6 @@ public class NotesListItem extends LinearLayout {
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
String title = data.getTitle();
mNoteTitle.setText(TextUtils.isEmpty(title) ? "" : title);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
@ -160,10 +99,6 @@ public class NotesListItem extends LinearLayout {
setBackground(data);
}
/**
*
* @param data 便
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
@ -181,10 +116,6 @@ public class NotesListItem extends LinearLayout {
}
}
/**
* 便
* @return 便
*/
public NoteItemData getItemData() {
return mItemData;
}

@ -1,422 +0,0 @@
package net.micode.notes.ui;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
* 便
* RecyclerView便
*/
public class NotesListItemAdapter extends RecyclerView.Adapter<NotesListItemAdapter.ViewHolder> {
/**
*
*/
private static final String TAG = "NotesListItemAdapter";
/**
*
*/
private Context mContext;
/**
*
*/
private Cursor mCursor;
/**
*
*/
private HashMap<Integer, Boolean> mSelectedIndex;
/**
* 便
*/
private int mNotesCount;
/**
*
*/
private boolean mChoiceMode;
/**
*
*/
private OnItemClickListener mOnItemClickListener;
/**
*
*/
private OnItemLongClickListener mOnItemLongClickListener;
/**
*
*/
public interface OnItemClickListener {
/**
*
* @param view
* @param position
*/
void onItemClick(View view, int position);
}
/**
*
*/
public interface OnItemLongClickListener {
/**
*
* @param view
* @param position
* @return
*/
boolean onItemLongClick(View view, int position);
}
/**
*
* @param listener
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
/**
*
* @param listener
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
mOnItemLongClickListener = listener;
}
/*
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
}
*/
/**
*
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
/**
*
* @param itemView
*/
public ViewHolder(View itemView) {
super(itemView);
}
}
/**
*
* @param context
*/
public NotesListItemAdapter(Context context) {
mContext = context;
mSelectedIndex = new HashMap<Integer, Boolean>();
mNotesCount = 0;
}
/**
*
* @param parent
* @param viewType
* @return
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
NotesListItem view = new NotesListItem(mContext);
view.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
final ViewHolder holder = new ViewHolder(view);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(v, holder.getAdapterPosition());
}
}
});
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (mOnItemLongClickListener != null) {
return mOnItemLongClickListener.onItemLongClick(v, holder.getAdapterPosition());
}
return false;
}
});
return holder;
}
/**
*
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (!mCursor.moveToPosition(position)) {
Log.e(TAG, "couldn't move cursor to position " + position);
return;
}
View view = holder.itemView;
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(mContext, mCursor);
((NotesListItem) view).bind(mContext, itemData, mChoiceMode,
isSelectedItem(position));
}
}
/**
*
* @return
*/
@Override
public int getItemCount() {
if (mCursor != null) {
return mCursor.getCount();
}
return 0;
}
/**
*
* @param position
* @param checked
*/
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked);
notifyItemChanged(position);
}
/**
*
* @return
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
*
* @param mode
*/
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear();
mChoiceMode = mode;
notifyDataSetChanged();
}
/**
*
* @param checked
*/
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getItemCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
setCheckedItem(i, checked);
}
}
}
notifyDataSetChanged();
}
/**
* ID
* @return ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
}
}
}
return itemSet;
}
/**
*
* @return
*/
public HashSet<DataUtils.AppWidgetAttribute> getSelectedWidget() {
HashSet<DataUtils.AppWidgetAttribute> itemSet = new HashSet<DataUtils.AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Cursor c = (Cursor) getItem(position);
if (c != null) {
DataUtils.AppWidgetAttribute widget = new DataUtils.AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
} else {
Log.e(TAG, "Invalid cursor");
return null;
}
}
}
return itemSet;
}
/**
*
* @return
*/
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
if (true == iter.next()) {
count++;
}
}
return count;
}
/**
*
* @return
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
* @param position
* @return
*/
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position);
}
/**
*
* @param cursor
*/
public void changeCursor(Cursor cursor) {
if (mCursor == cursor) {
return;
}
if (mCursor != null) {
mCursor.close();
}
mCursor = cursor;
if (mCursor != null) {
calcNotesCount();
}
notifyDataSetChanged();
}
/**
*
* @return
*/
public Cursor getCursor() {
return mCursor;
}
/**
*
* @param position
* @return
*/
public Object getItem(int position) {
if (mCursor != null && mCursor.moveToPosition(position)) {
return mCursor;
}
return null;
}
/**
* ID
* @param position
* @return ID
*/
public long getItemId(int position) {
if (mCursor != null && mCursor.moveToPosition(position)) {
// 使用标准的 NoteColumns.ID 常量,确保安全
int idColumnIndex = mCursor.getColumnIndex(net.micode.notes.data.Notes.NoteColumns.ID);
return mCursor.getLong(idColumnIndex);
}
return 0;
}
/**
* 便
*/
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getItemCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
Log.e(TAG, "Invalid cursor");
return;
}
}
}
/**
*
* @return
*/
public boolean isAllSelectedItemsPinned() {
if (mSelectedIndex == null || mSelectedIndex.size() == 0) {
return false;
}
// [Fix] 动态获取索引,比写死 13 更安全
int isPinnedColumnIndex = mCursor.getColumnIndex(Notes.NoteColumns.IS_PINNED);
if (isPinnedColumnIndex == -1) {
return false; // 如果查不到该列,默认视为未置顶
}
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position)) {
if (mCursor != null && mCursor.moveToPosition(position)) {
// 使用动态索引读取
int isPinned = mCursor.getInt(isPinnedColumnIndex);
if (isPinned <= 0) {
return false;
}
}
}
}
return true;
}
}

@ -14,13 +14,6 @@
* limitations under the License.
*/
/**
* 便
* <p>
*
* </p>
* @since 1.0
*/
package net.micode.notes.ui;
import android.accounts.Account;
@ -48,83 +41,34 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import android.net.Uri; // [新增] 修复错误 1 & 2
import android.provider.Settings;
import android.os.Build;
import android.preference.CheckBoxPreference;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
public class NotesPreferenceActivity extends PreferenceActivity {
/**
*
*/
public static final String PREFERENCE_NAME = "notes_preferences";
/**
*
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/**
*
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/**
*
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
/**
* 0: , 1:
*/
public static final String PREFERENCE_VIEW_MODE = "pref_view_mode"; // 0: 列表, 1: 瀑布流
/**
*
*/
public static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
/**
*
*/
private static final String AUTHORITIES_FILTER_KEY = "authorities";
/**
*
*/
private PreferenceCategory mAccountCategory;
/**
*
*/
private GTaskReceiver mReceiver;
private Account[] mOriAccounts;
/**
*
*/
private boolean mHasAddedAccount;
/**
*
*/
private static final int REQUEST_CODE_SELECT_BG = 1001; // [新增] 定义请求码
/**
*
*/
private static final int REQUEST_CODE_OVERLAY = 2001; // [新增] 悬浮窗权限请求码
/**
*
* @param icicle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
@ -133,70 +77,17 @@ public class NotesPreferenceActivity extends PreferenceActivity {
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
final CheckBoxPreference floatingPref = (CheckBoxPreference) findPreference("pref_key_enable_floating");
if (floatingPref != null) {
floatingPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
boolean isEnabled = (boolean) newValue;
if (isEnabled) {
// 尝试开启:首先检查系统权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(NotesPreferenceActivity.this)) {
// 没有权限,引导去系统设置页
Toast.makeText(NotesPreferenceActivity.this, R.string.toast_need_overlay_permission, Toast.LENGTH_LONG).show();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
// 2001 是我们自定义的请求码
startActivityForResult(intent, 2001);
return false; // 暂时不改变 CheckBox 状态,等权限授予后再变
} else {
// 有权限,直接启动服务
startService(new Intent(NotesPreferenceActivity.this, FloatingService.class));
}
} else {
// 关闭:停止服务
stopService(new Intent(NotesPreferenceActivity.this, FloatingService.class));
}
return true;
}
});
}
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
registerReceiver(mReceiver, filter);
mOriAccounts = null;
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
// 修改 onCreate 中的点击事件逻辑
Preference customBgPref = findPreference("pref_key_custom_list_bg");
if (customBgPref != null) {
customBgPref.setOnPreferenceClickListener(p -> {
// 使用 ACTION_OPEN_DOCUMENT 以获取可持久化的 URI 权限
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_SELECT_BG); // [核心修改] 使用传统启动方式
return true;
});
}
// 在 onCreate 绑定点击事件的地方添加:
Preference resetBgPref = findPreference("pref_key_reset_list_bg");
if (resetBgPref != null) {
resetBgPref.setOnPreferenceClickListener(p -> {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
settings.edit().remove("custom_list_background_uri").apply(); // 移除键值
Toast.makeText(this, "列表背景已恢复默认", Toast.LENGTH_SHORT).show();
return true;
});
}
}
/**
*
*/
@Override
protected void onResume() {
super.onResume();
@ -225,155 +116,178 @@ public class NotesPreferenceActivity extends PreferenceActivity {
refreshUI();
}
/**
*
*/
@Override
protected void onDestroy() {
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// --- 分支 A处理自定义列表背景选择 (原有逻辑) ---
if (requestCode == REQUEST_CODE_SELECT_BG && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
if (uri != null) {
try {
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
settings.edit().putString("custom_list_background_uri", uri.toString()).apply();
Toast.makeText(this, "列表背景更新成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "图片授权失败", Toast.LENGTH_SHORT).show();
}
}
}
// --- 分支 B处理悬浮窗权限返回 (新增逻辑) ---
else if (requestCode == REQUEST_CODE_OVERLAY) {
// 检查系统是否真的已经授予了权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) {
Toast.makeText(this, "悬浮窗权限已授予", Toast.LENGTH_SHORT).show();
// 1. 立即启动悬浮窗服务
startService(new Intent(this, FloatingService.class));
// 2. 找到 UI 控件,手动将开关设为【选中】
// 因为我们在 onPreferenceChange 中返回了 false所以需要在这里手动补上状态
CheckBoxPreference floatingPref = (CheckBoxPreference) findPreference("pref_key_enable_floating");
if (floatingPref != null) {
floatingPref.setChecked(true);
}
} else {
// 用户拒绝了权限或直接按了返回
Toast.makeText(this, "未获得权限,悬浮球无法开启", Toast.LENGTH_SHORT).show();
private void loadAccountPreference() {
mAccountCategory.removeAll();
// 确保 UI 开关保持在【关闭】状态
CheckBoxPreference floatingPref = (CheckBoxPreference) findPreference("pref_key_enable_floating");
if (floatingPref != null) {
floatingPref.setChecked(false);
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// the first time to set account
showSelectAccountAlertDialog();
} else {
// if the account has already been set, we need to promp
// user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
}
return true;
}
}
});
mAccountCategory.addPreference(accountPref);
}
/**
*
*/
private void loadAccountPreference() {
mAccountCategory.removeAll();
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
Preference accountPref = new Preference(this);
// [核心修改] 使用 Firebase 获取当前用户信息,不再使用 getSyncAccountName
com.google.firebase.auth.FirebaseUser user = com.google.firebase.auth.FirebaseAuth.getInstance().getCurrentUser();
if (user != null) {
// 已登录状态
accountPref.setTitle("当前登录账号");
accountPref.setSummary(user.getEmail());
accountPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 点击后弹出简单的信息确认,或者不做操作(注销已在主页菜单实现)
android.widget.Toast.makeText(NotesPreferenceActivity.this,
"账号已通过 Firebase 认证", android.widget.Toast.LENGTH_SHORT).show();
return true;
// set button state
if (GTaskSyncService.isSyncing()) {
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
}
});
} else {
// 未登录状态
accountPref.setTitle("账号未登录");
accountPref.setSummary("点击跳转至登录页面");
accountPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent intent = new Intent(NotesPreferenceActivity.this, LoginActivity.class);
startActivity(intent);
return true;
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
GTaskSyncService.startSync(NotesPreferenceActivity.this);
}
});
}
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
mAccountCategory.addPreference(accountPref);
// set last sync time
if (GTaskSyncService.isSyncing()) {
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
DateFormat.format(getString(R.string.preferences_last_sync_time_format),
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
/**
*
*/
private void loadSyncButton() {
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 始终显示“立即同步”
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setEnabled(true);
syncButton.setOnClickListener(new View.OnClickListener() {
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// 点击后执行我们的新同步
androidx.work.OneTimeWorkRequest syncRequest =
new androidx.work.OneTimeWorkRequest.Builder(net.micode.notes.sync.SyncWorker.class).build();
androidx.work.WorkManager.getInstance(NotesPreferenceActivity.this).enqueue(syncRequest);
Toast.makeText(NotesPreferenceActivity.this, "开始同步...", Toast.LENGTH_SHORT).show();
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
// 暂时隐藏旧的状态文字
lastSyncTimeView.setVisibility(View.GONE);
}
/**
* UI
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* Google
* @return
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
* @param account
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
@ -388,15 +302,22 @@ public class NotesPreferenceActivity extends PreferenceActivity {
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
@ -419,22 +340,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}).start();
}
/**
*
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
@ -443,22 +354,26 @@ public class NotesPreferenceActivity extends PreferenceActivity {
editor.commit();
}
/**
*
* @param context
* @return
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
*
* @param item
* @return
*/
private class GTaskReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
.getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
}
}
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:

@ -32,50 +32,19 @@ import net.micode.notes.tool.ResourceParser;
import net.micode.notes.ui.NoteEditActivity;
import net.micode.notes.ui.NotesListActivity;
/**
*
* <p>
*
*
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
/**
*
*/
public static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
/**
* ID
*/
public static final int COLUMN_ID = 0;
/**
* ID
*/
public static final int COLUMN_BG_COLOR_ID = 1;
/**
*
*/
public static final int COLUMN_SNIPPET = 2;
/**
*
*/
private static final String TAG = "NoteWidgetProvider";
/**
*
* <p>
* WIDGET_IDINVALID_APPWIDGET_ID
*
* @param context
* @param appWidgetIds ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
ContentValues values = new ContentValues();
@ -88,15 +57,6 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
}
/**
*
* <p>
* ID
*
* @param context
* @param widgetId ID
* @return Cursor
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
@ -105,29 +65,10 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
null);
}
/**
*
* <p>
* update
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
/**
*
* <p>
* ID
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
for (int i = 0; i < appWidgetIds.length; i++) {
@ -183,31 +124,9 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
}
/**
* ID
* <p>
* IDID
*
* @param bgId ID
* @return ID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
* <p>
* ID
*
* @return ID
*/
protected abstract int getLayoutId();
/**
*
* <p>
*
*
* @return
*/
protected abstract int getWidgetType();
}

@ -24,58 +24,22 @@ import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
/**
* 2x
* <p>
* 2x
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
/**
*
* <p>
* update
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* ID
* <p>
* 2xID
*
* @return ID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_2x;
}
/**
* ID
* <p>
* ID2xID
*
* @param bgId ID
* @return ID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId);
}
/**
*
* <p>
* 2x
*
* @return
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;

@ -24,57 +24,21 @@ import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
/**
* 4x
* <p>
* 4x
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
/**
*
* <p>
* update
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.update(context, appWidgetManager, appWidgetIds);
}
/**
* ID
* <p>
* 4xID
*
* @return ID
*/
protected int getLayoutId() {
return R.layout.widget_4x;
}
/**
* ID
* <p>
* ID4xID
*
* @param bgId ID
* @return ID
*/
@Override
protected int getBgResourceId(int bgId) {
return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId);
}
/**
*
* <p>
* 4x
*
* @return
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;

Loading…
Cancel
Save