维护 #8

Merged
profjqucz merged 2 commits from liuyan_branch into master 2 weeks ago

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 圆角矩形 -->
<corners android:radius="10dip" />
<!-- 背景颜色 -->
<solid android:color="#FF6666" />
<!-- 边框 -->
<stroke
android:width="1dip"
android:color="#FF4444" />
</shape>

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

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

@ -0,0 +1,15 @@
<?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">
<ListView
android:id="@+id/friend_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:drawable/divider_horizontal_bright"
android:dividerHeight="1dp"
android:padding="8dp" />
</LinearLayout>

@ -0,0 +1,73 @@
<?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">
<ScrollView
android:id="@+id/sv_note_edit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/note_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@android:color/darker_gray">
<TextView
android:id="@+id/tv_modified_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Medium"
android:textColor="@android:color/primary_text_light" />
</LinearLayout>
<!-- 标题显示区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="@android:color/white">
<TextView
android:id="@+id/note_title_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Large"
android:textColor="@android:color/primary_text_dark"
android:textSize="20sp"
android:gravity="center"
android:paddingBottom="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="sans-serif-medium"
android:background="@null" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC" />
</LinearLayout>
<!-- 正文显示区域 -->
<TextView
android:id="@+id/note_edit_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:padding="16dp"
android:textAppearance="@android:style/TextAppearance.Medium"
android:textColor="@android:color/primary_text_dark"
android:textSize="16sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

@ -0,0 +1,15 @@
<?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">
<ListView
android:id="@+id/friend_note_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@android:drawable/divider_horizontal_bright"
android:dividerHeight="1dp"
android:padding="8dp" />
</LinearLayout>

@ -0,0 +1,147 @@
<?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:gravity="center_horizontal"
android:padding="20dp"
android:background="@android:color/white">
<!-- 应用图标 -->
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher"
android:layout_marginBottom="30dp"
android:layout_marginTop="50dp" />
<!-- 应用名称 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="40dp" />
<!-- 登录表单 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal"
android:background="@android:color/white"
android:padding="20dp"
android:elevation="5dp"
android:layout_marginBottom="20dp">
<!-- 用户名输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="用户名:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入用户名"
android:inputType="text"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="20dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 登录按钮 -->
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="45dp"
android:text="登录"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_orange_light"
android:layout_marginBottom="15dp"
android:onClick="onLoginClick" />
<!-- 注册和忘记密码按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<Button
android:id="@+id/btn_register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="注册账号"
android:textSize="14sp"
android:textColor="@android:color/holo_blue_dark"
android:background="@null"
android:onClick="onRegisterClick" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="@android:color/darker_gray"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp" />
<Button
android:id="@+id/btn_change_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改密码"
android:textSize="14sp"
android:textColor="@android:color/holo_blue_dark"
android:background="@null"
android:onClick="onChangePasswordClick" />
</LinearLayout>
</LinearLayout>
<!-- 版权信息 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="© 2024 小米便签"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginTop="50dp" />
</LinearLayout>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:gravity="center">
<!-- 小米官方logo: 橙色圆形背景白色MI字样 -->
<LinearLayout
android:id="@+id/mi_logo"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_centerInParent="true"
android:background="@drawable/mi_logo_background"
android:gravity="center"
android:orientation="vertical">
<!-- 使用TextView显示白色MI字样 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MI"
android:textColor="#FFFFFF"
android:textSize="120sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
<!-- 移除应用名称只保留logo -->
</RelativeLayout>

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#f0f0f0">
<!-- 聊天消息列表 -->
<ListView
android:id="@+id/chat_list"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:cacheColorHint="@null"
android:divider="@null"
android:dividerHeight="8dip"
android:padding="8dip"
android:scrollbars="vertical" />
<!-- 输入框和发送按钮 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#ffffff"
android:orientation="horizontal"
android:padding="8dip"
android:weightSum="1">
<EditText
android:id="@+id/message_edit_text"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:background="@android:drawable/edit_text"
android:hint="输入消息..."
android:inputType="textMultiLine"
android:maxLines="3"
android:padding="8dip"
android:textColor="#000000"
android:textSize="16sp" />
<Button
android:id="@+id/send_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dip"
android:background="@android:drawable/btn_default"
android:text="发送"
android:textColor="#ffffff"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:gravity="left"
android:orientation="vertical"
android:padding="4dip">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="#ffffff"
android:orientation="vertical"
android:padding="8dip">
<TextView
android:id="@+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:maxWidth="250dip"
android:padding="4dip"
android:text="消息内容"
android:textColor="#000000"
android:textSize="16sp" />
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="4dip"
android:padding="2dip"
android:text="12:00"
android:textColor="#80000000"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:gravity="left"
android:orientation="vertical"
android:padding="4dip">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="#ffffff"
android:orientation="vertical"
android:padding="8dip">
<!-- 添加"便签"标注 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="[便签]"
android:textColor="#ffffff"
android:textSize="12sp"
android:background="@android:drawable/btn_default_small"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:layout_marginRight="4dip"
android:backgroundTint="#0000ff" /> <!-- 接收的便签标注使用蓝色 -->
<TextView
android:id="@+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:maxWidth="300dip"
android:padding="4dip"
android:text="便签内容"
android:textColor="#000000"
android:textSize="16sp"
android:clickable="true"
android:focusable="true"
android:background="@android:drawable/btn_default_small"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="4dip"
android:paddingBottom="4dip" />
</LinearLayout> <!-- 闭合LinearLayout -->
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="4dip"
android:padding="2dip"
android:text="12:00"
android:textColor="#80000000"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:orientation="vertical"
android:padding="4dip">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@android:drawable/btn_default"
android:orientation="vertical"
android:padding="8dip">
<TextView
android:id="@+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:maxWidth="250dip"
android:padding="4dip"
android:text="消息内容"
android:textColor="#ffffff"
android:textSize="16sp" />
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="4dip"
android:padding="2dip"
android:text="12:00"
android:textColor="#80ffffff"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:gravity="right"
android:orientation="vertical"
android:padding="4dip">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@android:drawable/btn_default"
android:orientation="vertical"
android:padding="8dip">
<!-- 添加"便签"标注 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:orientation="horizontal"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="[便签]"
android:textColor="#ffffff"
android:textSize="12sp"
android:background="@android:drawable/btn_default_small"
android:paddingLeft="6dip"
android:paddingRight="6dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:layout_marginRight="4dip"
android:backgroundTint="#ff0000" />
<TextView
android:id="@+id/message_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:maxWidth="300dip"
android:padding="4dip"
android:text="便签内容"
android:textColor="#ffffff"
android:textSize="16sp"
android:clickable="true"
android:focusable="true"
android:background="@android:drawable/btn_default_small"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="4dip"
android:paddingBottom="4dip" />
</LinearLayout> <!-- 闭合LinearLayout -->
<TextView
android:id="@+id/message_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_marginTop="4dip"
android:padding="2dip"
android:text="12:00"
android:textColor="#80ffffff"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- 标题 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改密码"
android:textSize="20sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="20dp" />
<!-- 用户名输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="用户名:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_change_username"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入用户名"
android:inputType="text"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 当前密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="当前密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_current_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入当前密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 新密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="新密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_new_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入新密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 确认新密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="20dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="确认新密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_confirm_new_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请再次输入新密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<Button
android:id="@+id/btn_change_cancel"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="取消"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="@android:drawable/btn_default"
android:layout_marginRight="20dp" />
<Button
android:id="@+id/btn_change_confirm"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="确认修改"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- 标题 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="注册账号"
android:textSize="20sp"
android:textStyle="bold"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="20dp" />
<!-- 用户名输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="用户名:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_register_username"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入用户名"
android:inputType="text"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="15dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_register_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请输入密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 确认密码输入 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="20dp">
<TextView
android:layout_width="80dp"
android:layout_height="wrap_content"
android:text="确认密码:"
android:textSize="16sp" />
<EditText
android:id="@+id/et_register_confirm_password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="请再次输入密码"
android:inputType="textPassword"
android:singleLine="true"
android:background="@android:drawable/editbox_background_normal"
android:paddingLeft="10dp"
android:paddingRight="10dp" />
</LinearLayout>
<!-- 按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<Button
android:id="@+id/btn_register_cancel"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="取消"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="@android:drawable/btn_default"
android:layout_marginRight="20dp" />
<Button
android:id="@+id/btn_register_confirm"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="注册"
android:textSize="16sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/friend_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Medium"
android:textColor="@android:color/primary_text_light"
android:textSize="16sp" />
</LinearLayout>

@ -27,18 +27,20 @@
android:orientation="vertical">
<LinearLayout
android:id="@+id/note_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
android:id="@+id/note_title"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_modified_date"
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" />
<TextView
android:id="@+id/tv_modified_date"
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"
@ -85,19 +87,59 @@
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent">
android:layout_height="fill_parent"
android:orientation="vertical">
<!-- 标题编辑区域 -->
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="10dip">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginBottom="5dip">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题:"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:layout_marginRight="10dip" />
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_title_edit"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="left"
android:background="@null"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:textSize="20sp"
android:singleLine="true"
android:inputType="textCapSentences|textAutoCorrect|textAutoComplete" />
</LinearLayout>
<View
android:layout_width="fill_parent"
android:layout_height="1dip"
android:background="#CCCCCC"
android:layout_marginTop="5dip" />
</LinearLayout>
<!-- 正文编辑区域 -->
<net.micode.notes.ui.NoteEditText
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_height="fill_parent"
android:gravity="left|top"
android:background="@null"
android:autoLink="all"
android:linksClickable="false"
android:minLines="12"
android:linksClickable="true"
android:padding="10dip"
android:minHeight="200dip"
android:textAppearance="@style/TextAppearancePrimaryItem"
android:lineSpacingMultiplier="1.2" />
android:lineSpacingMultiplier="1.2"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect|textAutoComplete" />
<LinearLayout
android:id="@+id/note_edit_list"
@ -113,6 +155,25 @@
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
<!-- 字数统计显示 -->
<LinearLayout
android:id="@+id/ll_character_count"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="bottom|left"
android:paddingLeft="10dip"
android:paddingBottom="10dip"
android:background="@null">
<TextView
android:id="@+id/tv_character_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|left"
android:text="当前字数0"
android:textAppearance="@style/TextAppearanceSecondaryItem" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -46,6 +46,21 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<!-- 分类标签 -->
<TextView
android:id="@+id/tv_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/category_bg"
android:textColor="@android:color/white"
android:textSize="12sp"
android:paddingLeft="8dip"
android:paddingRight="8dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:layout_marginRight="8dip"
android:text="其他" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dip"
@ -74,5 +89,25 @@
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"/>
android:layout_gravity="top|right"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"/>
<ImageView
android:id="@+id/iv_lock_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:layout_marginRight="32dp"
android:layout_marginTop="8dp"
android:visibility="gone"/>
<ImageView
android:id="@+id/iv_public_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:layout_marginRight="56dp"
android:layout_marginTop="8dp"
android:visibility="gone"/>
</FrameLayout>

@ -17,6 +17,7 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/list_background">
@ -26,26 +27,103 @@
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title_bar"
<!-- 搜索栏 -->
<LinearLayout
android:id="@+id/search_bar"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bar_bg"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="#FFEAD1AE"
android:textSize="@dimen/text_font_size_medium" />
<ListView
android:id="@+id/notes_list"
android:orientation="horizontal"
android:padding="5dip"
android:gravity="center_vertical">
<!-- 标题栏 -->
<TextView
android:id="@+id/tv_title_bar"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="#FFEAD1AE"
android:textSize="@dimen/text_font_size_medium" />
<!-- 搜索框 -->
<EditText
android:id="@+id/et_search"
android:layout_width="0dip"
android:layout_height="35dip"
android:layout_weight="1"
android:background="@android:drawable/edit_text"
android:hint="搜索便签..."
android:textColor="#000000"
android:textSize="14sp"
android:paddingLeft="5dip"
android:paddingRight="5dip"
android:visibility="gone"
android:imeOptions="actionSearch"
android:singleLine="true" />
<!-- 搜索按钮 -->
<ImageView
android:id="@+id/iv_search"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="@android:drawable/ic_menu_search"
android:contentDescription="搜索" />
<!-- 取消按钮 -->
<ImageView
android:id="@+id/iv_cancel"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="取消"
android:visibility="gone" />
<!-- 省略号按钮 -->
<ImageView
android:id="@+id/iv_menu_more"
android:layout_width="35dip"
android:layout_height="35dip"
android:background="@android:drawable/ic_menu_more"
android:contentDescription="更多选项" />
</LinearLayout>
<FrameLayout
android:id="@+id/notes_container"
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" />
android:layout_weight="1">
<!-- 列表视图 -->
<ListView
android:id="@+id/notes_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null" />
<!-- 宫格视图 -->
<GridView
android:id="@+id/notes_grid"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="@null"
android:listSelector="@android:color/transparent"
android:divider="@null"
android:fadingEdge="@null"
android:numColumns="2"
android:columnWidth="150dip"
android:verticalSpacing="15dip"
android:horizontalSpacing="15dip"
android:padding="15dip"
android:visibility="gone"
android:stretchMode="columnWidth" />
</FrameLayout>
</LinearLayout>
<Button

@ -0,0 +1,16 @@
<?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="16dp">
<EditText
android:id="@+id/password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="@string/preferences_new_password_hint"
android:layout_marginBottom="16dp" />
</LinearLayout>

@ -0,0 +1,52 @@
<?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="16dp">
<TextView
android:id="@+id/current_password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/preferences_current_password_label"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/current_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="@string/preferences_current_password_hint"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/new_password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/preferences_new_password_label"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="@string/preferences_new_password_hint"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/confirm_password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/preferences_confirm_password_label"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="@string/preferences_confirm_password_hint" />
</LinearLayout>

@ -49,4 +49,79 @@
<item
android:id="@+id/menu_delete_remind"
android:title="@string/menu_remove_remind" />
<item
android:id="@+id/menu_move"
android:title="@string/menu_move" />
<item
android:id="@+id/menu_add_picture"
android:title="@string/menu_add_picture" />
<item
android:id="@+id/menu_change_background"
android:title="@string/menu_change_background" />
<item
android:id="@+id/menu_undo"
android:title="撤回" />
<!-- 格式子菜单 -->
<item
android:id="@+id/menu_format"
android:title="格式"
android:showAsAction="ifRoom">
<menu>
<!-- 文字样式 -->
<group android:id="@+id/format_style">
<item
android:id="@+id/menu_format_bold"
android:title="加粗" />
<item
android:id="@+id/menu_format_italic"
android:title="斜体" />
<item
android:id="@+id/menu_format_underline"
android:title="下划线" />
<item
android:id="@+id/menu_format_strikethrough"
android:title="删除线" />
</group>
<!-- 文字颜色 -->
<group android:id="@+id/format_color">
<item
android:id="@+id/menu_format_text_color"
android:title="文字颜色" />
<item
android:id="@+id/menu_format_bg_color"
android:title="背景颜色" />
</group>
<!-- 对齐方式 -->
<group android:id="@+id/format_align">
<item
android:id="@+id/menu_format_align_left"
android:title="左对齐" />
<item
android:id="@+id/menu_format_align_center"
android:title="居中对齐" />
<item
android:id="@+id/menu_format_align_right"
android:title="右对齐" />
<item
android:id="@+id/menu_format_align_justify"
android:title="两端对齐" />
</group>
<!-- 字号大小 -->
<group android:id="@+id/format_font_size">
<item
android:id="@+id/menu_format_font_large"
android:title="大字号" />
<item
android:id="@+id/menu_format_font_normal"
android:title="正常字号" />
<item
android:id="@+id/menu_format_font_small"
android:title="小字号" />
</group>
</menu>
</item>
</menu>

@ -32,6 +32,10 @@
<item
android:id="@+id/menu_setting"
android:title="@string/menu_setting" />
<item
android:id="@+id/menu_change_background"
android:title="@string/menu_change_background" />
<item
android:id="@+id/menu_search"

@ -17,6 +17,14 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/pin"
android:title="置顶"
android:showAsAction="always|withText" />
<item
android:id="@+id/unpin"
android:title="取消置顶"
android:showAsAction="always|withText" />
<item
android:id="@+id/move"
android:title="@string/menu_move"
@ -28,4 +36,20 @@
android:title="@string/menu_delete"
android:icon="@drawable/menu_delete"
android:showAsAction="always|withText" />
<item
android:id="@+id/lock"
android:title="@string/note_lock_menu_lock"
android:showAsAction="always|withText" />
<item
android:id="@+id/unlock"
android:title="@string/note_lock_menu_unlock"
android:showAsAction="always|withText" />
<item
android:id="@+id/make_public"
android:title="开放"
android:showAsAction="always|withText" />
<item
android:id="@+id/make_private"
android:title="取消开放"
android:showAsAction="always|withText" />
</menu>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Notesmaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

@ -26,6 +26,8 @@
<string name="notelist_menu_new">Add note</string>
<string name="delete_remind_time_message">Delete reminder successfully</string>
<string name="set_remind_time_message">Set reminder</string>
<string name="note_alert_canceled">Reminder canceled</string>
<string name="note_alert_set_success">Reminder set successfully</string>
<string name="note_alert_expired">Expired</string>
<string name="format_date_ymd">yyyyMMdd</string>
<string name="format_datetime_mdhm">MMMd kk:mm</string>
@ -67,6 +69,8 @@
<string name="menu_send_to_desktop">Send to home</string>
<string name="menu_alert">Remind me</string>
<string name="menu_remove_remind">Delete reminder</string>
<string name="menu_add_picture">添加图片</string>
<string name="menu_change_background">更换背景</string>
<string name="menu_title_select_folder">Select folder</string>
<string name="menu_move_parent_folder">Parent folder</string>
<string name="info_note_enter_desktop">Note added to home</string>
@ -115,6 +119,26 @@
<string name="preferences_toast_cannot_change_account">Cannot change the account because sync is in progress</string>
<string name="preferences_toast_success_set_accout">%1$s has been set as the sync account</string>
<string name="preferences_bg_random_appear_title">New note background color random</string>
<string name="preferences_password_title">设置(修改)密码</string>
<string name="preferences_password_summary">设置或修改便签密码</string>
<string name="preferences_password_set_title">设置密码</string>
<string name="preferences_password_change_title">修改密码</string>
<string name="preferences_current_password_label">当前密码</string>
<string name="preferences_current_password_hint">请输入当前密码</string>
<string name="preferences_new_password_label">新密码</string>
<string name="preferences_new_password_hint">请输入新密码</string>
<string name="preferences_confirm_password_label">确认密码</string>
<string name="preferences_confirm_password_hint">请再次输入新密码</string>
<string name="preferences_button_confirm">确定</string>
<string name="preferences_button_cancel">取消</string>
<string name="preferences_password_incorrect">当前密码不正确</string>
<string name="preferences_password_empty">新密码不能为空</string>
<string name="preferences_password_not_match">两次输入的密码不一致</string>
<string name="preferences_password_set_success">密码设置成功</string>
<string name="note_lock_menu_lock">加锁</string>
<string name="note_lock_menu_unlock">解锁</string>
<string name="note_lock_password_prompt">请输入密码</string>
<string name="note_lock_password_incorrect">密码错误</string>
<string name="button_delete">Delete</string>
<string name="call_record_folder_name">Call notes</string>
@ -126,10 +150,11 @@
<string name="search">Notes</string>
<string name="datetime_dialog_ok">set</string>
<string name="datetime_dialog_cancel">cancel</string>
<string name="character_count">当前字数:%d</string>
<plurals name="search_results_title">
<item quantity="one"><xliff:g id="number" example="1">%1$s</xliff:g> result for \"<xliff:g id="search" example="???">%2$s</xliff:g>\"</item>
<item quantity="one"><xliff:g id="number" example="1">%1$s</xliff:g> result for "<xliff:g id="search" example="???">%2$s</xliff:g>"</item>
<!-- Case of 0 or 2 or more results. -->
<item quantity="other"><xliff:g id="number" example="15">%1$s</xliff:g> results for \"<xliff:g id="search" example="???">%2$s</xliff:g>\"</item>
<item quantity="other"><xliff:g id="number" example="15">%1$s</xliff:g> results for "<xliff:g id="search" example="???">%2$s</xliff:g>"</item>
</plurals>
</resources>

@ -49,7 +49,7 @@
</style>
<style name="HighlightTextAppearancePrimary">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
</style>
@ -63,7 +63,16 @@
</style>
<style name="NoteActionBarStyle" parent="@android:style/Widget.Holo.Light.ActionBar.Solid">
<item name="android:displayOptions" />
<item name="android:visibility">gone</item>
<item name="android:visibility">visible</item>
</style>
<style name="Theme.Transparent" parent="@android:style/Theme.Holo.Light.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Notesmaster" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />
</resources>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

@ -26,8 +26,8 @@ import android.util.Log;
import java.util.HashMap;
public class Contact {
private static HashMap<String, String> sContactCache;//定义sContactCache是一个缓存电话号码和相应联系人名字的哈希表
private static final String TAG = "Contact";//定义用于日志输出的标识TAG
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 + "'"
@ -35,37 +35,25 @@ public class Contact {
+ "(SELECT raw_contact_id "
+ " FROM phone_lookup"
+ " WHERE min_match = '+')";
//用于构建数据库查询条件的字符串常量
public static String getContact(Context context, String phoneNumber)
//参数:Context对象:用于访问系统服务和应用资源 phoneNumber:需要查询的联系人电话号码
{
public static String getContact(Context context, String phoneNumber) {
if(sContactCache == null) {
sContactCache = new HashMap<String, String>();
// 没映射表就建表,有就查缓存中有没有这个联系人
}
if(sContactCache.containsKey(phoneNumber)) {
return sContactCache.get(phoneNumber);
}
//返回从表里查询到的对应名字
String selection = CALLER_ID_SELECTION.replace("+",
PhoneNumberUtils.toCallerIDMinMatch(phoneNumber));
//构造一个SQL查询条件CALLER_ID_SELECTION中的"+"被替换为电话号码的最小匹配值
//然后执行查询语句
Cursor cursor = context.getContentResolver().query(
Data.CONTENT_URI,
new String [] { Phone.DISPLAY_NAME },
selection,
new String[] { phoneNumber },
null);
//判断查询结果:
//查询结果不为空,且能够移动到第一条记录:
// 那么就尝试从Cursor中获取联系人姓名并将其存入缓存sContactCache。然后返回联系人姓名。
// 异常情况如果在获取字符串时发生数组越界异常则记录一个错误日志并返回null。
// 最后都要确保关闭Cursor对象以避免内存泄漏。
//如果查询结果为空或者没有记录可以移动到(即没有找到匹配的联系人):
// 则记录一条调试日志并返回null
if (cursor != null && cursor.moveToFirst()) {
try {
String name = cursor.getString(0);

@ -0,0 +1,23 @@
package net.micode.notes.data;
public class Messages {
public interface MessageColumns {
public static final String ID = "id";
public static final String SENDER_ID = "sender_id";
public static final String RECEIVER_ID = "receiver_id";
public static final String CONTENT = "content";
public static final String MESSAGE_TYPE = "message_type";
public static final String CREATED_DATE = "created_date";
public static final String IS_READ = "is_read";
}
public interface MessageType {
public static final int TEXT = 0;
public static final int IMAGE = 1;
public static final int EMOTION = 2;
public static final int NOTE = 3;
}
public static final long ID_INVALID = -1;
}

@ -18,11 +18,8 @@ package net.micode.notes.data;
import android.net.Uri;
public class Notes {
//用于表示笔记应用中的各种类型、标识符以及Intent的额外数据
public static final String AUTHORITY = "micode_notes";
public static final String TAG = "Notes";
//对NoteColumns.TYPE的值进行设置时使用
//即不同种类:笔记、文件夹和系统文件夹
public static final int TYPE_NOTE = 0;
public static final int TYPE_FOLDER = 1;
public static final int TYPE_SYSTEM = 2;
@ -33,18 +30,11 @@ public class Notes {
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
*/
//以下id是系统文件夹的标识符即系统文件夹的分类
//ID_ROOT_FOLDER默认文件夹
//ID_TEMPARAY_FOLDER不属于文件夹的笔记
//ID_CALL_RECORD_FOLDER用于存储通话记录以便返回
//ID_TRASH_FOLER垃圾回收站
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;
// 额外的数据键个人理解为就是定义一些布局的ID
// 这部分就是用于设置UI界面的一些布局或小组件的id给它定义成常量了。
// 这样的封装性可能比较好因为如果有部分要修改则直接来这边修改即可不用在activity部分一个一个修改。
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
@ -55,14 +45,12 @@ public class Notes {
public static final int TYPE_WIDGET_INVALIDE = -1;
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
// 数据常量:里面定义了两种类型:文本便签和通话记录
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
}
//定义一堆访问笔记和文件的uri
//GPTAndroid开发中常见的用于定义内容提供者Content ProviderURI
//内容提供者是一种Android组件它允许应用程序共享和存储数据。这里定义了一个URI来查询数据
/**
* Uri to query all notes and folders
*/
@ -109,8 +97,13 @@ public class Notes {
* Folder's name or text content of note
* <P> Type: TEXT </P>
*/
// 摘要
public static final String SNIPPET = "snippet";
/**
* Note's title
* <P> Type: TEXT </P>
*/
public static final String TITLE = "title";
/**
* Note's widget id
@ -153,8 +146,6 @@ public class Notes {
* The last sync id
* <P> Type: INTEGER (long) </P>
*/
//在数据同步过程中这个ID可能用来跟踪和识别每次同步操作的唯一性确保数据的一致性。
public static final String SYNC_ID = "sync_id";
/**
@ -180,12 +171,39 @@ public class Notes {
* <P> Type : INTEGER (long) </P>
*/
public static final String VERSION = "version";
/**
* Whether the note is pinned
* <P> Type: INTEGER (1=pinned, 0=unpinned) </P>
*/
public static final String PINNED = "pinned";
/**
* Sort order for notes in the same folder
* <P> Type: INTEGER </P>
*/
public static final String SORT_ORDER = "sort_order";
/**
* Whether the note is locked
* <P> Type: INTEGER (1=locked, 0=unlocked) </P>
*/
public static final String LOCKED = "locked";
/**
* The user id that this note belongs to
* <P> Type: INTEGER (long) </P>
*/
public static final String USER_ID = "user_id";
/**
* Whether the note is public
* <P> Type: INTEGER (1=public, 0=private) </P>
*/
public static final String PUBLIC = "public";
}
public interface DataColumns {
// DataColumns的接口这个接口包含了一系列静态常量这些常量代表了数据库表中用于存储数据的列名。
// 每个常量都有相应的注释,说明该列的作用和数据类型。
/**
* The unique ID for a row
* <P> Type: INTEGER (long) </P>
@ -196,42 +214,32 @@ public class Notes {
* The MIME type of the item represented by this row.
* <P> Type: Text </P>
*/
//MIME类型是一种标准用于标识文档、文件或字节流的性质和格式。在数据库中这个字段可以用来识别不同类型的数据例如文本、图片、音频或视频等。
public static final String MIME_TYPE = "mime_type";
/**
* The reference id to note that this data belongs to
* <P> Type: INTEGER (long) </P>
*/
//归属的Note的ID
public static final String NOTE_ID = "note_id";
/**
* Created data for note or folder
* <P> Type: INTEGER (long) </P>
*/
//创建日期
public static final String CREATED_DATE = "created_date";
/**
* Latest modified date
* <P> Type: INTEGER (long) </P>
*/
//最近修改日期
public static final String MODIFIED_DATE = "modified_date";
/**
* Data's content
* <P> Type: TEXT </P>
*/
//数据内容
public static final String CONTENT = "content";
// 以下5个是通用数据列它们的具体意义取决于MIME类型由MIME_TYPE字段指定
// 不同的MIME类型可能需要存储不同类型的数据这五个字段提供了灵活性允许根据MIME类型来存储相应的数据。
// 读后面的代码感觉这部分是在表示内容的不同状态
/**
* Generic data column, the meaning is {@link #MIMETYPE} specific, used for
@ -269,41 +277,39 @@ public class Notes {
public static final String DATA5 = "data5";
}
//以下是文本便签的定义
public static final class TextNote implements DataColumns {
/**
* 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;//模式这个被存在DATA1列中
public static final String MODE = DATA1;
public static final int MODE_CHECK_LIST = 1;//所处检查列表模式
public static final int MODE_CHECK_LIST = 1;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/text_note";// 定义了MIME类型用于标识文本标签的目录
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";// 定义了MIME类型用于标识文本标签的单个项
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");//文本标签内容提供者Content Provider的URI用于访问文本标签数据
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
// 通话记录的定义
public static final class CallNote implements DataColumns {
/**
* Call date for this record
* <P> Type: INTEGER (long) </P>
*/
public static final String CALL_DATE = DATA1;//一个字符串常量,表示通话记录的日期
public static final String CALL_DATE = DATA1;
/**
* Phone number for this record
* <P> Type: TEXT </P>
*/
public static final String PHONE_NUMBER = DATA3;//意味着在数据库表中这个电话号码信息将被存储在DATA3列中
public static final String PHONE_NUMBER = DATA3;
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/call_note";// 同样定义了MIME类型是用于标识通话记录的目录。
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";// 同样定义了MIME类型是用于标识通话记录的单个项
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/call_note";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");//定义了通话记录内容提供者的URI用于访问通话记录数据
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/call_note");
}
}

@ -1,3 +1,19 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
@ -9,256 +25,223 @@ import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Messages;
import net.micode.notes.data.Users;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 数据库帮助类,用于管理名为 note.db 的 SQLite 数据库。
// 它继承自 SQLiteOpenHelper 类,这是 Android提供的一个方便的工具类用于管理数据库的创建和版本更新.
// 数据库的基本信息;数据库名称和版本信息(在创建实例对象时会用到)
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 12;
//内部接口个人理解为两个表名一个note一个data
public interface TABLE {
public static final String NOTE = "note";
public static final String DATA = "data";
public static final String USER = "user";
public static final String MESSAGE = "message";
}
//一个标签,方便日志输出时识别出信息来自哪里
private static final String TAG = "NotesDatabaseHelper";
//静态所有变量,提供一个全局访问点来获取数据库辅助类的唯一实例,使得在应用的任何地方都可以方便地使用它
private static NotesDatabaseHelper mInstance;
/* 以下都是一些SQL语句辅助我们来对数据库进行操作 */
//创建note表的语句这里的NoteColumns就是我们刚刚在Notes中定义的一个接口里面定义了一系列静态的数据库表中的列名
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
//同上创建data表的语句这里的DataColumns就是我们刚刚在Notes中定义的一个接口里面定义了一系列静态的数据库表中的列名
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TITLE + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," +
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.SORT_ORDER + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCKED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.USER_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.PUBLIC + " INTEGER NOT NULL DEFAULT 0" +
")";
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
// 功能简介:
// 创建一个以note的ID为索引
// 解读:
// 用于在TABLE.DATA表上创建一个名为note_id_index的索引。
// 这个索引是基于DataColumns.NOTE_ID列的。IF NOT EXISTS确保了如果索引已经存在那么就不会尝试重新创建它避免了可能的错误。
// 索引通常用于提高查询性能,特别是在对某个字段进行频繁查询时。
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
/* 以下是一些对便签增删改定义的触发器 */
/*
* NOTEDATA
* NOTE
* */
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
private static final String CREATE_USER_TABLE_SQL =
"CREATE TABLE " + TABLE.USER + "(" +
Users.UserColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Users.UserColumns.USERNAME + " TEXT NOT NULL UNIQUE," +
Users.UserColumns.PASSWORD + " TEXT NOT NULL," +
Users.UserColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
Users.UserColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")";
private static final String CREATE_MESSAGE_TABLE_SQL =
"CREATE TABLE " + TABLE.MESSAGE + "(" +
Messages.MessageColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Messages.MessageColumns.SENDER_ID + " INTEGER NOT NULL," +
Messages.MessageColumns.RECEIVER_ID + " INTEGER NOT NULL," +
Messages.MessageColumns.CONTENT + " TEXT NOT NULL," +
Messages.MessageColumns.MESSAGE_TYPE + " INTEGER NOT NULL DEFAULT 0," +
Messages.MessageColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
Messages.MessageColumns.IS_READ + " INTEGER NOT NULL DEFAULT 0" +
")";
/**
* Increase folder's note count when move note to the folder
*/
// 功能简介:
// 添加触发器:增加文件夹的便签个数记录(因为我们会移动便签进入文件夹,这时候文件夹的计数要进行更新)
// 解读:
// 定义了一个SQL触发器increase_folder_count_on_update。
// 触发器是一种特殊的存储过程它会在指定表上的指定事件如INSERT、UPDATE、DELETE发生时自动执行。
// 这个触发器会在TABLE.NOTE表的NoteColumns.PARENT_ID字段更新后执行。
// 触发器的逻辑是当某个笔记的PARENT_ID即父文件夹ID被更新时它会找到对应的文件夹通过新的PARENT_ID并将该文件夹的NOTES_COUNT即笔记数增加1。
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when move note from folder
*/
// 功能简介:(触发器和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:减少文件夹的便签个数记录(因为我们会移动便签移出文件夹,这时候文件夹的计数要进行更新)
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
* Increase folder's note count when insert new note to the folder
*/
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹插入便签时,增加文件夹的便签个数记录
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* Decrease folder's note count when delete note from the folder
*/
// 功能简介:(触发器原理和上面的 “增加文件夹的便签个数记录” 同理,就不细节解读了)
// 添加触发器:当我们在文件夹删除便签时,减少文件夹的便签个数记录
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
/**
* Update note's content when insert data with type {@link DataConstants#NOTE}
*/
// 功能简介:
// 添加触发器当向DATA表中插入类型为NOTE便签的数据时更新note表对应的笔记内容。
// 解读:
// 在DATA表上进行INSERT操作后如果新插入的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与新插入数据相关联的标签的SNIPPET摘要字段设置为新插入数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has changed
*/
// 功能简介:
// 添加触发器当DATA表中类型为NOTE便签的数据更改时更新note表对应的笔记内容。
// 解读:
// 在DATA表上进行UPDATE操作后如果更新前的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与更新后的数据相关联的笔记的SNIPPET字段设置为新数据的CONTENT字段的值
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Update note's content when data with {@link DataConstants#NOTE} type has deleted
*/
// 功能简介:
// 添加触发器当DATA表中类型为NOTE便签的数据删除时更新note表对应的笔记内容置空
// 解读:
// 在DATA表上进行DELETE操作后如果删除的数据的MIME_TYPE为NOTE则触发此操作。
// 它会更新NOTE表将与删除的数据相关联的笔记的SNIPPET字段设置为空字符串。
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* Delete datas belong to note which has been deleted
*/
// 功能简介:
// 添加触发器当从NOTE表中删除笔记时删除与该笔记相关联的数据就是删除data表中为该note的数据
// 解读:
// 在NOTE表上进行DELETE操作后此触发器被激活。
// 它会从DATA表中删除所有与已删除的笔记由old.ID表示相关联的数据行通过比较DATA表中的NOTE_ID字段与已删除笔记的ID来实现
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Delete notes belong to folder which has been deleted
*/
// 功能简介:
// 添加触发器当从NOTE表中删除一个文件夹时删除该文件夹下的所有笔记。
// 解读:
// 在NOTE表上进行DELETE操作后如果删除的是一个文件夹由old.ID表示
// 触发器会删除所有以该文件夹为父级PARENT_ID的笔记通过比较NOTE表中的PARENT_ID字段与已删除文件夹的ID来实现
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* Move notes belong to folder which has been moved to trash folder
*/
// 功能简介:
// 添加触发器:当某个文件夹被移动到回收站时,移动该文件夹下的所有笔记到回收站
// 解读:
// 在NOTE表上进行UPDATE操作后如果某个文件夹的新PARENT_ID字段值等于回收站的IDNotes.ID_TRASH_FOLER
// 触发器会更新所有以该文件夹为父级PARENT_ID的笔记将它们也移动到回收站。
private static final String FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER =
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
// 构造器
"CREATE TRIGGER folder_move_notes_on_trash " +
" AFTER UPDATE ON " + TABLE.NOTE +
" WHEN new." + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.PARENT_ID + "=" + Notes.ID_TRASH_FOLER +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
// 创建note标签
public void createNoteTable(SQLiteDatabase db) {
db.execSQL(CREATE_NOTE_TABLE_SQL);
reCreateNoteTableTriggers(db);
@ -266,9 +249,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "note table has been created");
}
// 重新创建或更新与笔记表相关的触发器。
// 首先使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前不存在同名的触发器。
// 然后使用db.execSQL()方法执行预定义的SQL语句这些语句用于创建新的触发器。
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");
@ -287,17 +267,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(FOLDER_MOVE_NOTES_ON_TRASH_TRIGGER);
}
/* 以下部分是操作SQLite数据库部分 */
// 功能简介:
// 创建通话记录文件夹、默认文件夹、临时文件夹和回收站,并插入相关数据
// 具体解读:
// ContentValues是一个用于存储键值对的类常用于SQLite数据库的插入操作
// values.put方法可以向ContentValues对象中添加数据。
// NoteColumns.ID是存储文件夹ID的列名Notes.ID_CALL_RECORD_FOLDER是通话记录文件夹的ID。
// NoteColumns.TYPE是存储文件夹类型的列名Notes.TYPE_SYSTEM表示这是一个系统文件夹。
// 使用db.insert方法将values中的数据插入到TABLE.NOTE即标签表中。
// 每次插入新数据前都使用values.clear()方法清除ContentValues对象中的旧数据确保不会重复插入旧数据。
// 然后分别创建默认文件夹、临时文件夹和回收站,并以同样的方法插入数据。
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
@ -311,7 +280,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* root folder which is default folder
*/
// 创建默认文件夹:重复上述步骤,但这次是为根文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@ -320,7 +288,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* temporary folder which is used for moving note
*/
// 创建“临时”文件夹:同样地,为临时文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
@ -329,21 +296,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* create trash folder
*/
// 创建“回收站”文件夹:最后,为回收站文件夹插入数据。
values.clear();
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
//功能简介:
//创建data数据
//解读:
//这个方法用于创建数据表,以及与之相关的触发器。
//创建数据表使用db.execSQL方法执行预定义的SQL语句CREATE_DATA_TABLE_SQL用于创建数据表。
//重新创建数据表触发器调用reCreateDataTableTriggers方法用于删除并重新创建与数据表相关的触发器。
//创建索引使用db.execSQL方法执行CREATE_DATA_NOTE_ID_INDEX_SQL语句为数据表创建索引。
//记录日志使用Log.d方法记录一条调试级别的日志表示数据表已经创建。
public void createDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_DATA_TABLE_SQL);
reCreateDataTableTriggers(db);
@ -351,10 +309,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "data table has been created");
}
//和上面的note表的reCreate...同理
//重新创建或更新与笔记表相关的触发器。
//首先使用DROP TRIGGER IF EXISTS语句删除已存在的触发器。确保在重新创建触发器之前不存在同名的触发器。
//然后使用db.execSQL()方法执行预定义的SQL语句这些语句用于创建新的触发器。
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");
@ -365,31 +319,31 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
//解读:
//synchronized关键字确保在多线程环境下只有一个线程能够进入这个方法防止了同时创建多个实例的情况
//getInstance(Context context)方法使用了单例模式来确保整个应用程序中只有一个NotesDatabaseHelper实例。
//它首先检查mInstance类的静态成员变量没有在代码片段中显示是否为null。
//如果是null则创建一个新的NotesDatabaseHelper实例并将其赋值给mInstance。最后返回mInstance。
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
//功能简介:
//当数据库首次创建时onCreate方法会被调用。
//这里重写onCreate方法它调用了上述createNoteTable(db)和createDataTable(db)两个方法
//这样首次创建数据库时就多出了两张表。
public void createUserTable(SQLiteDatabase db) {
db.execSQL(CREATE_USER_TABLE_SQL);
Log.d(TAG, "user table has been created");
}
public void createMessageTable(SQLiteDatabase db) {
db.execSQL(CREATE_MESSAGE_TABLE_SQL);
Log.d(TAG, "message table has been created");
}
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
createUserTable(db);
createMessageTable(db);
}
//功能简介:
//当数据库需要升级时即数据库的版本号改变onUpgrade方法会被调用。
//该方法会根据当前的oldVersion和新的newVersion来执行相应的升级操作
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
boolean reCreateTriggers = false;
@ -412,22 +366,57 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
if (oldVersion == 5) {
upgradeToV6(db);
oldVersion++;
}
if (oldVersion == 6) {
upgradeToV7(db);
oldVersion++;
}
if (oldVersion == 7) {
createUserTable(db);
oldVersion++;
}
if (oldVersion == 8) {
upgradeToV9(db);
oldVersion++;
}
if (oldVersion == 9) {
upgradeToV10(db);
oldVersion++;
}
if (oldVersion == 10) {
upgradeToV11(db);
oldVersion++;
}
if (oldVersion == 11) {
createMessageTable(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
if (oldVersion != newVersion) { //数据库升级失败,抛出一个异常,表示数据库升级失败
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
//功能简介:
// 将数据库从版本1升级到版本2。
//解读:
// 首先它删除了已经存在的NOTE和DATA表如果存在的话。DROP TABLE IF EXISTS语句确保了即使这些表不存在也不会抛出错误。
// 然后它调用了createNoteTable(db)和createDataTable(db)方法来重新创建这两个表。这意味着在升级到版本2时这两个表的内容会被完全清除并重新创建新的空表。
private void upgradeToV2(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
@ -435,12 +424,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createDataTable(db);
}
//功能简介:
// 将数据库从版本2或可能是跳过版本2的某个状态升级到版本3。
//解读:
// 首先,删除了三个不再使用的触发器(如果存在的话)。触发器是数据库中的一种对象,可以在插入、更新或删除记录时自动执行某些操作。
// 然后使用ALTER TABLE语句修改表结构向NOTE表中添加了一个名为GTASK_ID的新列并设置默认值为空字符串。
// 最后向NOTE表中插入了一条新的系统文件夹记录表示一个名为“trash folder”的系统文件夹。这可能是用于存储已删除笔记的回收站功能。
private void upgradeToV3(SQLiteDatabase db) {
// drop unused triggers
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
@ -456,12 +439,38 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.insert(TABLE.NOTE, null, values);
}
//功能简介:
// 这个方法负责将数据库从版本3升级到版本4。
//解读:
// 它向NOTE表中添加了一个名为VERSION的新列并设置了默认值为0。这个新列用于记录标签版本信息。
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV6(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.SORT_ORDER
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV7(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCKED
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV9(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.USER_ID
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV10(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PUBLIC
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV11(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TITLE
+ " TEXT NOT NULL DEFAULT ''");
}
}

@ -1,3 +1,19 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
@ -17,27 +33,16 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.tool.UserManager;
public class NotesProvider extends ContentProvider {
// Android 应用程序中的一部分内容提供者ContentProvider
// 内容提供者是 Android 四大组件之一,它允许应用程序之间共享数据。
//概述:
//NotesProvider的主要功能是作为一个内容提供者为其他应用程序或组件提供对“Notes”数据的访问。
//它允许其他应用程序查询、插入、更新或删除标签数据。
//通过URI匹配NotesProvider能够区分对哪种数据类型的请求例如单独的标签、标签的数据、文件夹操作等并执行相应的操作。
//用于匹配不同URI的UriMatcher对象通常用于解析传入的URI并确定应该执行哪种操作。
private static final UriMatcher mMatcher;
//NotesDatabaseHelper实类用来操作SQLite数据库负责创建、更新和查询数据库。
private NotesDatabaseHelper mHelper;
//标签,输出日志时用来表示是该类发出的消息
private static final String TAG = "NotesProvider";
//6个URI的匹配码用于区分不同的URI类型
private static final int URI_NOTE = 1;
private static final int URI_NOTE_ITEM = 2;
private static final int URI_DATA = 3;
@ -46,23 +51,13 @@ public class NotesProvider extends ContentProvider {
private static final int URI_SEARCH = 5;
private static final int URI_SEARCH_SUGGEST = 6;
//进一步定义了URI匹配规则和搜索查询的投影
//功能概述:
//初始化了一个UriMatcher对象mMatcher并添加了一系列的URI匹配规则。
//解读:
static {
//创建了一个UriMatcher实例并设置默认匹配码为NO_MATCH表示如果没有任何URI匹配则返回这个码。
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//添加规则当URI的authority为Notes.AUTHORITY路径为note时返回匹配码URI_NOTE。
mMatcher.addURI(Notes.AUTHORITY, "note", URI_NOTE);
//添加规则当URI的authority为Notes.AUTHORITY路径为note/后跟一个数字(#代表数字返回匹配码URI_NOTE_ITEM。
mMatcher.addURI(Notes.AUTHORITY, "note/#", URI_NOTE_ITEM);
//和上面两句同理但用于匹配数据相关的URI
mMatcher.addURI(Notes.AUTHORITY, "data", URI_DATA);
mMatcher.addURI(Notes.AUTHORITY, "data/#", URI_DATA_ITEM);
//用于匹配搜索相关的URI
mMatcher.addURI(Notes.AUTHORITY, "search", URI_SEARCH);
//这两行用于匹配搜索建议相关的URI
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST);
mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST);
}
@ -71,147 +66,236 @@ public class NotesProvider extends ContentProvider {
* x'0A' represents the '\n' character in sqlite. For title and content in the search result,
* we will trim '\n' and white space in order to show more information.
*/
//功能概述:
//一个 SQL 查询的投影部分,用于定义查询返回的结果集中应该包含哪些列。
//解读:(每行对应)
//返回笔记的 ID。
//笔记的 ID 也被重命名为 SUGGEST_COLUMN_INTENT_EXTRA_DATA这通常用于 Android 的搜索建议中,作为传递给相关 Intent 的额外数据。
//对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n替换为空字符串然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_1
//对 SNIPPET 列的处理:首先使用 REPLACE 函数将 x'0A'(即换行符 \n替换为空字符串然后使用 TRIM 函数删除前后的空白字符,处理后的结果分别重命名为 SUGGEST_COLUMN_TEXT_2
//返回一个用于搜索建议图标的资源 ID并命名为 SUGGEST_COLUMN_ICON_1。
//返回一个固定的 Intent 动作 ACTION_VIEW并命名为 SUGGEST_COLUMN_INTENT_ACTION。
//返回一个内容类型,并命名为 SUGGEST_COLUMN_INTENT_DATA。
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + "," //返回笔记的 ID
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
private static final String NOTES_SEARCH_PROJECTION = NoteColumns.ID + ","
+ NoteColumns.ID + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_1 + ","
+ "TRIM(REPLACE(" + NoteColumns.SNIPPET + ", x'0A','')) AS " + SearchManager.SUGGEST_COLUMN_TEXT_2 + ","
+ R.drawable.search_result + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1 + ","
+ "'" + Intent.ACTION_VIEW + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_ACTION + ","
+ "'" + Notes.TextNote.CONTENT_TYPE + "' AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA;
//功能概述:
//完整的 SQL 查询语句,用于从 TABLE.NOTE 表中检索信息
//解读:
// 使用上面定义的投影来选择数据。
// 并指定从哪个表中选择数据。
//WHERE子句包含三个条件
// ①搜索 SNIPPET 列中包含特定模式的行(? 是一个占位符,实际查询时会用具体的值替换)。
// ②父ID不为回收站的ID排除那些父 ID 为回收站的行。
// ③只选择类型为note标签的行。
private static String NOTES_SNIPPET_SEARCH_QUERY = "SELECT " + NOTES_SEARCH_PROJECTION
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
+ " FROM " + TABLE.NOTE
+ " WHERE " + NoteColumns.SNIPPET + " LIKE ?"
+ " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER
+ " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
//重写onCreate方法
//getContext() 方法被调用以获取当前组件的上下文Context以便 NotesDatabaseHelper 能够访问应用程序的资源和其他功能
//mHelper用于存储从 NotesDatabaseHelper.getInstance 方法返回的实例。这样,该实例就可以在整个组件的其他方法中被访问和使用。
@Override
public boolean onCreate() {
mHelper = NotesDatabaseHelper.getInstance(getContext());
return true;
}
//功能:查询数据
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
//初始化变量:
//Cursor对象 c用来存储查询结果
//使用 NotesDatabaseHelper 的实例 mHelper来获取一个可读的数据库实例
//定义一个字符串id用来存储从URI中解析出的ID
String sortOrder) {
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
SQLiteDatabase db = null;
String id = null;
//根据匹配不同的URI来进行不同的查询
switch (mMatcher.match(uri)) {
// URI_NOTE查询整个 NOTE 表。
// URI_NOTE_ITEM查询 NOTE 表中的特定项。ID 从 URI 的路径段中获取,并添加到查询条件中。
// URI_DATA查询整个 DATA 表。
// URI_DATA_ITEM查询 DATA 表中的特定项。ID 的获取和处理方式与 URI_NOTE_ITEM 相同。
case URI_NOTE:
c = db.query(TABLE.NOTE, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
id = uri.getPathSegments().get(1);
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
//URI_SEARCH 和 URI_SEARCH_SUGGEST处理搜索查询。
// 代码首先检查是否提供了不应与搜索查询一起使用的参数(如 sortOrder, selection, selectionArgs, 或 projection
// 如果提供了这些参数,则抛出一个 IllegalArgumentException。
// 根据 URI 类型,从 URI 的路径段或查询参数中获取搜索字符串 searchString。
// 如果 searchString 为空或无效,则返回 null表示没有搜索结果。
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
try {
db = mHelper.getReadableDatabase();
// 获取当前用户ID添加空指针检查
long currentUserId = 0;
if (getContext() != null) {
currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
}
// 构建用户ID过滤条件确保currentUserId不为-1
long userIdForFilter = currentUserId > 0 ? currentUserId : 0;
String userFilter = NoteColumns.USER_ID + " = " + userIdForFilter;
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 构建查询条件:
// 1. 如果selection中包含USER_ID条件说明是查询特定用户的便签直接使用selection
// 2. 如果selection中包含PUBLIC条件说明是查询公开便签直接使用selection
// 3. 否则,系统文件夹(ID <= 0)不受用户ID限制普通便签需要匹配当前用户ID
String finalSelection;
if (TextUtils.isEmpty(selection)) {
finalSelection = "(" + NoteColumns.ID + " <= 0) OR (" + userFilter + ")";
} else {
// 检查selection中是否包含USER_ID或PUBLIC条件
if (selection.contains(NoteColumns.USER_ID) || selection.contains(NoteColumns.PUBLIC)) {
// 如果包含USER_ID或PUBLIC条件直接使用selection绕过当前用户过滤
finalSelection = selection;
} else {
// 否则添加用户ID过滤
finalSelection = "((" + NoteColumns.ID + " <= 0) OR (" + userFilter + ")) AND (" + selection + ")";
}
}
c = db.query(TABLE.NOTE, projection, finalSelection, selectionArgs, null, null,
sortOrder);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
// 对于单个便签详情,不添加用户过滤条件,允许访问任何便签
// 因为FriendNoteListActivity已经确保只显示公开便签
if (TextUtils.isEmpty(selection)) {
selection = null;
}
c = db.query(TABLE.NOTE, projection, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
break;
case URI_DATA:
// 处理DATA表查询需要考虑便签的PUBLIC属性
if (selection != null && selection.contains(DataColumns.NOTE_ID)) {
// 如果查询包含NOTE_ID需要确保对应的便签是公开的或属于当前用户
try {
// 从selection中提取note_id值
String noteIdStr = selection;
noteIdStr = noteIdStr.substring(noteIdStr.indexOf('=') + 1);
if (noteIdStr.contains("AND")) {
noteIdStr = noteIdStr.substring(0, noteIdStr.indexOf("AND")).trim();
}
long noteId = Long.parseLong(noteIdStr);
// 查询对应便签的PUBLIC和USER_ID属性
Cursor noteCursor = db.query(
TABLE.NOTE,
new String[]{NoteColumns.PUBLIC, NoteColumns.USER_ID},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null,
null,
null
);
boolean canAccess = false;
if (noteCursor != null && noteCursor.moveToFirst()) {
int isPublic = noteCursor.getInt(0);
long noteUserId = noteCursor.getLong(1);
// 允许访问的条件:便签是公开的或便签属于当前用户
canAccess = (isPublic == 1 || noteUserId == currentUserId);
noteCursor.close();
}
if (!canAccess) {
// 如果不能访问返回空cursor
return null;
}
} catch (Exception e) {
// 如果解析失败,默认允许访问,避免误判
Log.e(TAG, "Error parsing note_id from selection: " + e.getMessage());
}
}
c = db.query(TABLE.DATA, projection, selection, selectionArgs, null, null,
sortOrder);
break;
case URI_DATA_ITEM:
// 处理单个DATA项查询同样需要考虑便签的PUBLIC属性
id = uri.getPathSegments().get(1);
// 先查询该DATA项对应的note_id
Cursor dataCursor = null;
boolean canAccess = false;
try {
dataCursor = db.query(
TABLE.DATA,
new String[]{DataColumns.NOTE_ID},
DataColumns.ID + "=?",
new String[]{id},
null,
null,
null
);
if (dataCursor != null && dataCursor.moveToFirst()) {
long noteId = dataCursor.getLong(0);
// 查询对应便签的PUBLIC和USER_ID属性
Cursor noteCursor = null;
try {
noteCursor = db.query(
TABLE.NOTE,
new String[]{NoteColumns.PUBLIC, NoteColumns.USER_ID},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null,
null,
null
);
if (noteCursor != null && noteCursor.moveToFirst()) {
int isPublic = noteCursor.getInt(0);
long noteUserId = noteCursor.getLong(1);
// 允许访问的条件:便签是公开的或便签属于当前用户
canAccess = (isPublic == 1 || noteUserId == currentUserId);
}
} finally {
if (noteCursor != null) {
noteCursor.close();
}
}
}
} finally {
if (dataCursor != null) {
dataCursor.close();
}
}
if (canAccess) {
c = db.query(TABLE.DATA, projection, DataColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs, null, null, sortOrder);
}
// 如果不能访问返回null默认处理
break;
case URI_SEARCH:
case URI_SEARCH_SUGGEST:
if (sortOrder != null || projection != null) {
throw new IllegalArgumentException(
"do not specify sortOrder, selection, selectionArgs, or projection" + "with this query");
}
} else {
searchString = uri.getQueryParameter("pattern");
}
if (TextUtils.isEmpty(searchString)) {
return null;
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
}
} else {
searchString = uri.getQueryParameter("pattern");
}
//字符串格式化:格式化后的字符串就会是 "%s%"即包含s是任何文本
//然后执行原始SQL查询
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
if (TextUtils.isEmpty(searchString)) {
return null;
}
//未知URI处理
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果查询结果不为空(即 Cursor 对象 c 不是 null则为其设置一个通知 URI。
//这意味着当与这个 URI 关联的数据发生变化时,任何注册了监听这个 URI 的 ContentObserver 都会被通知。
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
try {
searchString = String.format("%%%s%%", searchString);
c = db.rawQuery(NOTES_SNIPPET_SEARCH_QUERY,
new String[] { searchString });
} catch (IllegalStateException ex) {
Log.e(TAG, "got exception: " + ex.toString());
}
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (c != null) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
} catch (Exception e) {
Log.e(TAG, "query exception: " + e.toString());
e.printStackTrace();
}
return c;
}
//功能:插入数据
//参数Uri 用来标识要插入数据的表ContentValues对象包含要插入的键值对
@Override
public Uri insert(Uri uri, ContentValues values) {
//获取数据库
//三个长整型变量分别用来存储数据项ID、便签ID 和插入行的ID
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
//对于 URI_NOTE将values插入到 TABLE.NOTE 表中,并返回插入行的 ID。
//对于 URI_DATA首先检查values是否包含 DataColumns.NOTE_ID如果包含则获取其值。如果不包含记录一条日志信息。然后将 values 插入到 TABLE.DATA 表中,并返回插入行的 ID。
//如果 uri 不是已知的 URI 类型,则抛出一个 IllegalArgumentException。
// 获取当前用户ID添加空指针检查
long currentUserId = 0;
if (getContext() != null) {
currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
}
switch (mMatcher.match(uri)) {
case URI_NOTE:
// 插入新便签时添加当前用户ID
values.put(NoteColumns.USER_ID, currentUserId);
insertedId = noteId = db.insert(TABLE.NOTE, null, values);
break;
case URI_DATA:
@ -225,10 +309,6 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//功能:通知变化
//如果noteId 或 dataId 大于 0即成功插入了数据则使用 ContentResolver 的 notifyChange 方法通知监听这些 URI 的观察者,告知数据已经改变。
//ContentUris.withAppendedId 方法用于在基本 URI 后面追加一个 ID形成完整的 URI。
// Notify the note uri
if (noteId > 0) {
getContext().getContentResolver().notifyChange(
@ -241,29 +321,33 @@ public class NotesProvider extends ContentProvider {
ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), null);
}
//返回包含新插入数据项ID 的 Uri。允许调用者知道新插入的数据项的位置
return ContentUris.withAppendedId(uri, insertedId);
}
//功能:删除数据项
//参数uri标识要删除数据的表或数据项。 selection一个可选的 WHERE 子句,用于指定删除条件。 selectionArgs一个可选的字符串数组用于替换 selection 中的占位符
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//count记录被删除的行数。
//id用于存储从 URI 中解析出的数据项 ID。
//db可写的数据库对象用于执行删除操作。
//deleteData一个布尔值用于标记是否删除了 DATA 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean deleteData = false;
// 获取当前用户ID添加空指针检查
long currentUserId = 0;
if (getContext() != null) {
currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
}
// 构建用户ID过滤条件
String userFilter = NoteColumns.USER_ID + " = " + currentUserId;
switch (mMatcher.match(uri)) {
//URI_NOTE: 修改 selection 语句:确保只删除 ID 大于 0 的笔记。然后执行删除操作并返回被删除的行数。
//URI_NOTE_ITEM: 从 URI 中解析出 ID。检查 ID 是否小于等于 0如果是则不执行删除操作否则执行删除操作并返回被删除的行数
//URI_DATA 执行删除操作并返回被删除的行数。设置 deleteData 为 true表示删除了 DATA 表中的数据。
//URI_DATA_ITEM 先从 URI 中解析出 ID然后执行删除操作并返回被删除的行数并设置 deleteData 为 true表示删除了 DATA 表中的数据。
case URI_NOTE:
// 添加用户ID过滤
if (TextUtils.isEmpty(selection)) {
selection = userFilter;
} else {
selection = userFilter + " AND (" + selection + ")";
}
selection = "(" + selection + ") AND " + NoteColumns.ID + ">0 ";
count = db.delete(TABLE.NOTE, selection, selectionArgs);
break;
@ -277,6 +361,12 @@ public class NotesProvider extends ContentProvider {
if (noteId <= 0) {
break;
}
// 添加用户ID过滤
if (TextUtils.isEmpty(selection)) {
selection = userFilter;
} else {
selection = userFilter + " AND (" + selection + ")";
}
count = db.delete(TABLE.NOTE,
NoteColumns.ID + "=" + id + parseSelection(selection), selectionArgs);
break;
@ -293,45 +383,45 @@ public class NotesProvider extends ContentProvider {
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果 count 大于 0说明有数据被删除。
//如果 deleteData 为 true则通知监听 Notes.CONTENT_NOTE_URI 的观察者,数据已改变。
//通知监听传入 uri 的观察者数据已改变。
if (count > 0) {
if (deleteData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
}
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
//功能:更新数据库的数据
//参数uri标识要更新数据的表或数据项。 values一个包含新值的键值对集合。
// selection一个可选的 WHERE 子句,用于指定更新条件。 selectionArgs一个可选的字符串数组用于替换 selection 中的占位符。
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//count记录被更新的行数。
//id用于存储从 URI 中解析出的数据项 ID。
//db可写的 SQLite 数据库对象,用于执行更新操作。
//updateData用于标记是否更新了 data 表中的数据。
int count = 0;
String id = null;
SQLiteDatabase db = mHelper.getWritableDatabase();
boolean updateData = false;
// 获取当前用户ID添加空指针检查
long currentUserId = 0;
if (getContext() != null) {
currentUserId = UserManager.getInstance(getContext()).getCurrentUserId();
}
// 构建用户ID过滤条件
String userFilter = NoteColumns.USER_ID + " = " + currentUserId;
switch (mMatcher.match(uri)) {
//URI_NOTE调用 increaseNoteVersion 方法用于增加便签版本然后在note表执行更新操作并返回被更新的行数。
//URI_NOTE_ITEM从 URI 中解析出 ID并调用 increaseNoteVersion 方法,传入解析出的 ID最后在note表执行更新操作并返回被更新的行数。
//URI_DATA在data表执行更新操作并返回被更新的行数。设置 updateData 为 true表示更新了 DATA 表中的数据。
//URI_DATA_ITEM从 URI 中解析出 ID。执行更新操作并返回被更新的行数。置 updateData 为 true表示更新了 DATA 表中的数据。
case URI_NOTE:
// 添加用户ID过滤
if (TextUtils.isEmpty(selection)) {
selection = userFilter;
} else {
selection = userFilter + " AND (" + selection + ")";
}
increaseNoteVersion(-1, selection, selectionArgs);
count = db.update(TABLE.NOTE, values, selection, selectionArgs);
break;
case URI_NOTE_ITEM:
id = uri.getPathSegments().get(1);
// 对于单个便签更新NoteColumns.ID是主键已经足够唯一不需要添加用户ID过滤
increaseNoteVersion(Long.valueOf(id), selection, selectionArgs);
count = db.update(TABLE.NOTE, values, NoteColumns.ID + "=" + id
+ parseSelection(selection), selectionArgs);
@ -350,9 +440,6 @@ public class NotesProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown URI " + uri);
}
//如果 count 大于 0说明有数据被更新。
//如果 updateData 为 true则通知监听 Notes.CONTENT_NOTE_URI 的观察者数据已改变。
//通知监听传入 uri 的观察者数据已改变。
if (count > 0) {
if (updateData) {
getContext().getContentResolver().notifyChange(Notes.CONTENT_NOTE_URI, null);
@ -362,12 +449,10 @@ public class NotesProvider extends ContentProvider {
return count;
}
//解析传入的条件语句:一个 SQL WHERE 子句的一部分
private String parseSelection(String selection) {
return (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
}
//更新note表的version列将其值增加 1。
private void increaseNoteVersion(long id, String selection, String[] selectionArgs) {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
@ -399,4 +484,4 @@ public class NotesProvider extends ContentProvider {
return null;
}
}
}

@ -0,0 +1,31 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
public class Users {
public interface UserColumns {
public static final String ID = "_id";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String CREATED_DATE = "created_date";
public static final String MODIFIED_DATE = "modified_date";
}
public interface UserConstants {
// 可以添加一些用户相关的常量
}
}

@ -16,124 +16,67 @@
package net.micode.notes.gtask.data;
import android.database.Cursor; // 数据库查询游标:父类方法参数,此类未实际使用
import android.util.Log; // 日志工具:打印错误/警告日志
import android.database.Cursor;
import android.util.Log;
import net.micode.notes.tool.GTaskStringUtils; // GTask字符串工具类存储元数据相关常量如Gid字段名
import net.micode.notes.tool.GTaskStringUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONException; // JSON解析异常JSON操作失败时抛出
import org.json.JSONObject; // JSON对象存储和解析元数据键值对
/**
* GTask
* GTaskGTaskTask
* 便GTaskGTaskID
* JSONID
*/
public class MetaData extends Task {
// 日志标签:使用类名作为标签,方便定位该类的日志信息
private final static String TAG = MetaData.class.getSimpleName();
// 关联的远程GTask唯一ID用来绑定本地便签和远程GTask任务null表示未关联
private String mRelatedGid = null;
/**
* GTask ID
* GTaskIDJSONJSON
* 便GTask
* @param gid GTaskID
* @param metaInfo JSON
*/
public void setMeta(String gid, JSONObject metaInfo) {
try {
// 将远程GTask ID存入JSON对象键为元数据头部GTask ID常量
metaInfo.put(GTaskStringUtils.META_HEAD_GTASK_ID, gid);
} catch (JSONException e) {
// JSON存入失败打印错误日志
Log.e(TAG, "failed to put related gid");
}
// 将元信息JSON对象转为字符串设置为当前任务的备注存储元数据
setNotes(metaInfo.toString());
// 将任务名称设为元数据专用名称,标识这是元数据任务
setName(GTaskStringUtils.META_NOTE_NAME);
}
/**
* GTaskID
* 便GTaskID
* @return GTask IDnull
*/
public String getRelatedGid() {
return mRelatedGid;
}
/**
*
* JSONGTask
* @return true=false=
*/
@Override
public boolean isWorthSaving() {
return getNotes() != null;
}
/**
* GTaskJSON
* GTaskGTask ID
* @param js GTaskJSON
*/
@Override
public void setContentByRemoteJSON(JSONObject js) {
// 先调用父类方法解析通用的GTask任务内容如名称、备注等
super.setContentByRemoteJSON(js);
// 如果任务备注元数据JSON字符串不为空
if (getNotes() != null) {
try {
// 将备注字符串转为JSON对象去除首尾空白字符避免解析失败
JSONObject metaInfo = new JSONObject(getNotes().trim());
// 从JSON对象中提取关联的远程GTask ID
mRelatedGid = metaInfo.getString(GTaskStringUtils.META_HEAD_GTASK_ID);
} catch (JSONException e) {
// JSON解析失败打印警告日志并将关联ID置为null
Log.w(TAG, "failed to get related gid");
mRelatedGid = null;
}
}
}
/**
* JSON
* MetaDataGTaskJSON
* 访
*/
@Override
public void setContentByLocalJSON(JSONObject js) {
// 抛出非法访问错误,提示该方法不应该被调用
// this function should not be called
throw new IllegalAccessError("MetaData:setContentByLocalJSON should not be called");
}
/**
* JSON
* MetaDataJSON访
* @return
*/
@Override
public JSONObject getLocalJSONFromContent() {
// 抛出非法访问错误,提示该方法不应该被调用
throw new IllegalAccessError("MetaData:getLocalJSONFromContent should not be called");
}
/**
*
* MetaData访
* @param c 使
* @return
*/
@Override
public int getSyncAction(Cursor c) {
// 抛出非法访问错误,提示该方法不应该被调用
throw new IllegalAccessError("MetaData:getSyncAction should not be called");
}
}
}

@ -16,147 +16,86 @@
package net.micode.notes.gtask.data;
import android.database.Cursor; // 数据库查询游标:用于子类查询本地数据库,判断同步动作
import android.database.Cursor;
import org.json.JSONObject; // JSON对象用于封装GTask接口所需参数或存储本地数据
import org.json.JSONObject;
/**
* GTask
* GTask
* ID
* GTask
*/
public abstract class Node {
// 同步动作常量:表示无任何同步操作(本地和远程数据一致)
public static final int SYNC_ACTION_NONE = 0;
// 同步动作常量往远程GTask服务器添加新数据本地有新增远程没有
public static final int SYNC_ACTION_ADD_REMOTE = 1;
// 同步动作常量:往本地数据库添加新数据(远程有新增,本地没有)
public static final int SYNC_ACTION_ADD_LOCAL = 2;
// 同步动作常量删除远程GTask服务器上的数据本地已删除远程还存在
public static final int SYNC_ACTION_DEL_REMOTE = 3;
// 同步动作常量:删除本地数据库里的数据(远程已删除,本地还存在)
public static final int SYNC_ACTION_DEL_LOCAL = 4;
// 同步动作常量更新远程GTask服务器上的数据本地修改比远程新
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;
// 远程GTask唯一标识Gid每个同步数据在GTask服务器上的唯一IDnull表示未上传到远程
private String mGid;
// 节点名称:同步数据的名称(比如便签标题、文件夹名称)
private String mName;
// 最后修改时间戳:数据最后一次被修改的时间(用于判断本地和远程哪个更新)
private long mLastModified;
// 删除标记是否被标记为删除true=已删除,需要同步删除操作)
private boolean mDeleted;
/**
*
* Node
*/
public Node() {
mGid = null; // 默认未关联远程GTask无Gid
mName = ""; // 默认名称为空字符串
mLastModified = 0; // 默认最后修改时间为0未修改
mDeleted = false; // 默认未被删除
mGid = null;
mName = "";
mLastModified = 0;
mDeleted = false;
}
/**
* JSON
* GTaskJSON
* GTask便
* @param actionId IDGTask
* @return JSON
*/
public abstract JSONObject getCreateAction(int actionId);
/**
* JSON
* GTaskJSON
* GTask便
* @param actionId IDGTask
* @return JSON
*/
public abstract JSONObject getUpdateAction(int actionId);
/**
* GTaskJSON
* GTask
* JSON便
* @param js GTaskJSON
*/
public abstract void setContentByRemoteJSON(JSONObject js);
/**
* JSON
* JSON
* JSON便
* @param js JSON
*/
public abstract void setContentByLocalJSON(JSONObject js);
/**
* JSON
* 便
* JSON便
* @return JSON
*/
public abstract JSONObject getLocalJSONFromContent();
/**
*
* Cursor
*
* @param c
* @return SYNC_ACTION_XXX
*/
public abstract int getSyncAction(Cursor c);
// 以下是通用属性的Setter方法设置对应的属性值供子类或外部调用
/** 设置远程GTask的唯一标识Gid */
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;
}
// 以下是通用属性的Getter方法获取对应的属性值供子类或外部调用
/** 获取远程GTask的唯一标识Gid */
public String getGid() {
return this.mGid;
}
/** 获取节点名称(如便签标题、文件夹名称) */
public String getName() {
return this.mName;
}
/** 获取最后修改时间戳(用于同步对比新旧) */
public long getLastModified() {
return this.mLastModified;
}
/** 获取删除标记(判断是否需要同步删除操作) */
public boolean getDeleted() {
return this.mDeleted;
}
}
}

@ -35,260 +35,155 @@ import org.json.JSONException;
import org.json.JSONObject;
/**
* 便
* 便
*
*/
public class SqlData {
// 日志标签:打印这个类的日志时,用来标识日志来源
private static final String TAG = SqlData.class.getSimpleName();
// 无效的编号用来标记数据还没有有效的唯一ID
private static final int INVALID_ID = -99999;
// 查询数据明细表时,要获取的字段清单(相当于要查的列名)
public static final String[] PROJECTION_DATA = new String[] {
DataColumns.ID, // 数据唯一ID
DataColumns.MIME_TYPE, // 数据类型(比如普通便签、通话便签)
DataColumns.CONTENT, // 数据内容(比如便签正文)
DataColumns.DATA1, // 扩展字段1存一些额外信息比如便签模式
DataColumns.DATA3 // 扩展字段3存一些额外信息
DataColumns.ID, DataColumns.MIME_TYPE, DataColumns.CONTENT, DataColumns.DATA1,
DataColumns.DATA3
};
// 上面字段清单对应的索引位置,方便从查询结果里快速取值
public static final int DATA_ID_COLUMN = 0; // 数据ID在查询结果里的位置
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; // 扩展字段1在查询结果里的位置
public static final int DATA_CONTENT_DATA_3_COLUMN = 4; // 扩展字段3在查询结果里的位置
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;
// 是否是新数据true=新创建的还没存到数据库false=已存在的(从数据库加载的)
private boolean mIsCreate;
// 这条数据的唯一ID
private long mDataId;
// 这条数据的类型(比如普通便签、通话便签)
private String mDataMimeType;
// 这条数据的内容(比如便签的正文文字)
private String mDataContent;
// 扩展字段1的值存额外信息比如便签是普通模式还是清单模式
private long mDataContentData1;
// 扩展字段3的值存额外信息
private String mDataContentData3;
// 数据变更记录:用来存和原来不一样的内容(只更新有变化的部分,提高效率)
private ContentValues mDiffDataValues;
/**
*
* @param context
*/
public SqlData(Context context) {
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为新数据(还没存数据库)
mIsCreate = true;
// 初始化为无效ID还没有数据库分配的唯一ID
mDataId = INVALID_ID;
// 初始数据类型为普通便签
mDataMimeType = DataConstants.NOTE;
// 初始内容为空字符串
mDataContent = "";
// 初始扩展字段1的值为0
mDataContentData1 = 0;
// 初始扩展字段3的值为空字符串
mDataContentData3 = "";
// 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
}
/**
*
* @param context
* @param c
*/
public SqlData(Context context, Cursor c) {
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为已有数据(不是新创建的)
mIsCreate = false;
// 从查询结果里加载数据到当前对象
loadFromCursor(c);
// 初始化数据变更记录容器
mDiffDataValues = new ContentValues();
}
/**
*
*
* @param c
*/
private void loadFromCursor(Cursor c) {
// 从查询结果里取数据ID
mDataId = c.getLong(DATA_ID_COLUMN);
// 从查询结果里取数据类型
mDataMimeType = c.getString(DATA_MIME_TYPE_COLUMN);
// 从查询结果里取数据内容
mDataContent = c.getString(DATA_CONTENT_COLUMN);
// 从查询结果里取扩展字段1的值
mDataContentData1 = c.getLong(DATA_CONTENT_DATA_1_COLUMN);
// 从查询结果里取扩展字段3的值
mDataContentData3 = c.getString(DATA_CONTENT_DATA_3_COLUMN);
}
/**
* JSON
* JSON
*
* @param js JSON
* @throws JSONException JSON
*/
public void setContent(JSONObject js) throws JSONException {
// 从JSON里取数据ID没有的话就用无效ID
long dataId = js.has(DataColumns.ID) ? js.getLong(DataColumns.ID) : INVALID_ID;
// 如果是新数据或者当前ID和JSON里的ID不一样就记录这个变更
if (mIsCreate || mDataId != dataId) {
mDiffDataValues.put(DataColumns.ID, dataId);
}
// 更新当前数据ID
mDataId = dataId;
// 从JSON里取数据类型没有的话就默认是普通便签
String dataMimeType = js.has(DataColumns.MIME_TYPE) ? js.getString(DataColumns.MIME_TYPE)
: DataConstants.NOTE;
// 如果是新数据或者当前类型和JSON里的类型不一样就记录这个变更
if (mIsCreate || !mDataMimeType.equals(dataMimeType)) {
mDiffDataValues.put(DataColumns.MIME_TYPE, dataMimeType);
}
// 更新当前数据类型
mDataMimeType = dataMimeType;
// 从JSON里取数据内容没有的话就为空字符串
String dataContent = js.has(DataColumns.CONTENT) ? js.getString(DataColumns.CONTENT) : "";
// 如果是新数据或者当前内容和JSON里的内容不一样就记录这个变更
if (mIsCreate || !mDataContent.equals(dataContent)) {
mDiffDataValues.put(DataColumns.CONTENT, dataContent);
}
// 更新当前数据内容
mDataContent = dataContent;
// 从JSON里取扩展字段1的值没有的话就为0
long dataContentData1 = js.has(DataColumns.DATA1) ? js.getLong(DataColumns.DATA1) : 0;
// 如果是新数据或者当前扩展字段1和JSON里的不一样就记录这个变更
if (mIsCreate || mDataContentData1 != dataContentData1) {
mDiffDataValues.put(DataColumns.DATA1, dataContentData1);
}
// 更新当前扩展字段1的值
mDataContentData1 = dataContentData1;
// 从JSON里取扩展字段3的值没有的话就为空字符串
String dataContentData3 = js.has(DataColumns.DATA3) ? js.getString(DataColumns.DATA3) : "";
// 如果是新数据或者当前扩展字段3和JSON里的不一样就记录这个变更
if (mIsCreate || !mDataContentData3.equals(dataContentData3)) {
mDiffDataValues.put(DataColumns.DATA3, dataContentData3);
}
// 更新当前扩展字段3的值
mDataContentData3 = dataContentData3;
}
/**
* JSON
* JSON便使
* @return JSON
* @throws JSONException JSON
*/
public JSONObject getContent() throws JSONException {
// 如果是新数据还没存到数据库打印错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
// 创建空的JSON数据包
JSONObject js = new JSONObject();
// 把数据ID存入JSON
js.put(DataColumns.ID, mDataId);
// 把数据类型存入JSON
js.put(DataColumns.MIME_TYPE, mDataMimeType);
// 把数据内容存入JSON
js.put(DataColumns.CONTENT, mDataContent);
// 把扩展字段1的值存入JSON
js.put(DataColumns.DATA1, mDataContentData1);
// 把扩展字段3的值存入JSON
js.put(DataColumns.DATA3, mDataContentData3);
// 返回打包好的JSON
return js;
}
/**
*
*
* @param noteId 便ID便
* @param validateVersion 便
* @param version 便
*/
public void commit(long noteId, boolean validateVersion, long version) {
// 如果是新数据(要插入数据库)
if (mIsCreate) {
// 如果数据ID是无效的就把ID从变更记录里移除数据库会自动生成唯一ID
if (mDataId == INVALID_ID && mDiffDataValues.containsKey(DataColumns.ID)) {
mDiffDataValues.remove(DataColumns.ID);
}
// 把所属的便签ID存入变更记录关联到便签主表
mDiffDataValues.put(DataColumns.NOTE_ID, noteId);
// 把变更记录里的内容插入到数据明细表返回新数据的URI
Uri uri = mContentResolver.insert(Notes.CONTENT_DATA_URI, mDiffDataValues);
try {
// 从返回的URI里提取数据库分配的唯一数据ID
mDataId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 提取ID失败打印错误日志并抛出异常
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);
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,
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) });
+ " WHERE " + NoteColumns.VERSION + "=?)", new String[] {
String.valueOf(noteId), String.valueOf(version)
});
}
// 如果更新结果为0说明没有更新成功可能版本不匹配或数据已被修改
if (result == 0) {
Log.w(TAG, "there is no update. maybe user updates note when syncing");
}
}
}
// 清空变更记录(本次提交完成,下次变更重新记录)
mDiffDataValues.clear();
// 标记为非新数据(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
/**
* ID
* @return ID
*/
public long getId() {
return mDataId;
}
}
}

@ -38,313 +38,222 @@ import org.json.JSONObject;
import java.util.ArrayList;
/**
* 便
* 便便
* 便便
* 便便
*/
public class SqlNote {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = SqlNote.class.getSimpleName();
// 无效的便签ID用来标记便签还没有有效的唯一编号
private static final int INVALID_ID = -99999;
// 查询便签主表时,要获取的字段清单(相当于要查的列名,对应便签的各项基本信息)
public static final String[] PROJECTION_NOTE = new String[] {
NoteColumns.ID, // 便签唯一ID
NoteColumns.ALERTED_DATE, // 提醒时间
NoteColumns.BG_COLOR_ID, // 背景颜色ID
NoteColumns.CREATED_DATE, // 创建时间
NoteColumns.HAS_ATTACHMENT, // 是否有附件0=没有1=有)
NoteColumns.MODIFIED_DATE, // 最后修改时间
NoteColumns.NOTES_COUNT, // 子便签数量
NoteColumns.PARENT_ID, // 所属文件夹ID
NoteColumns.SNIPPET, // 便签摘要(标题/部分正文)
NoteColumns.TYPE, // 便签类型(普通便签/文件夹/系统文件夹)
NoteColumns.WIDGET_ID, // 桌面小组件ID
NoteColumns.WIDGET_TYPE, // 桌面小组件类型
NoteColumns.SYNC_ID, // 同步编号
NoteColumns.LOCAL_MODIFIED, // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID, // 原始所属文件夹ID
NoteColumns.GTASK_ID, // 对应云端GTask的ID
NoteColumns.VERSION // 便签版本号(用来避免同步冲突)
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; // 便签ID在查询结果里的位置
public static final int ALERTED_DATE_COLUMN = 1; // 提醒时间在查询结果里的位置
public static final int BG_COLOR_ID_COLUMN = 2; // 背景颜色ID在查询结果里的位置
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; // 所属文件夹ID在查询结果里的位置
public static final int SNIPPET_COLUMN = 8; // 便签摘要在查询结果里的位置
public static final int TYPE_COLUMN = 9; // 便签类型在查询结果里的位置
public static final int WIDGET_ID_COLUMN = 10; // 桌面小组件ID在查询结果里的位置
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; // 原始所属文件夹ID在查询结果里的位置
public static final int GTASK_ID_COLUMN = 15; // 云端GTaskID在查询结果里的位置
public static final int VERSION_COLUMN = 16; // 便签版本号在查询结果里的位置
// 上下文:用来获取数据库操作工具、默认配置等
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;
// 是否是新便签true=新创建的还没存到数据库false=已存在的(从数据库加载的)
private boolean mIsCreate;
// 便签唯一ID
private long mId;
// 提醒时间比如设置了早上8点提醒这里存的是对应的时间戳
private long mAlertDate;
// 背景颜色ID对应不同的便签背景色比如白色、黄色
private int mBgColorId;
// 创建时间(存的是时间戳,记录便签什么时候被创建)
private long mCreatedDate;
// 是否有附件0=没有附件1=有附件)
private int mHasAttachment;
// 最后修改时间(存的是时间戳,记录便签最后一次被修改的时间)
private long mModifiedDate;
// 所属文件夹ID标记这个便签在哪个文件夹里
private long mParentId;
// 便签摘要(一般是便签的标题,或者正文的前几句)
private String mSnippet;
// 便签类型(普通便签/文件夹/系统文件夹)
private int mType;
// 桌面小组件ID如果这个便签添加到桌面这里存小组件的编号
private int mWidgetId;
// 桌面小组件类型(标记桌面小组件的样式)
private int mWidgetType;
// 原始所属文件夹ID记录便签最初在哪个文件夹里
private long mOriginParent;
// 便签版本号(每次修改都会递增,用来避免同步时多人同时修改导致冲突)
private long mVersion;
// 便签主表的变更记录:只存和原来不一样的内容,后续只更新这些变更,提高效率
private ContentValues mDiffNoteValues;
// 便签的明细数据列表比如普通便签的正文内容用SqlData对象存储
private ArrayList<SqlData> mDataList;
/**
* 便
* @param context
*/
public SqlNote(Context context) {
mContext = context;
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为新便签(还没存数据库)
mIsCreate = true;
// 初始化为无效ID还没有数据库分配的唯一ID
mId = INVALID_ID;
// 初始提醒时间为0没有设置提醒
mAlertDate = 0;
// 初始背景色为系统默认背景色
mBgColorId = ResourceParser.getDefaultBgId(context);
// 初始创建时间为当前时间(获取系统当前时间戳)
mCreatedDate = System.currentTimeMillis();
// 初始没有附件
mHasAttachment = 0;
// 初始最后修改时间为当前时间
mModifiedDate = System.currentTimeMillis();
// 初始所属文件夹ID为0默认在根目录
mParentId = 0;
// 初始摘要为空字符串
mSnippet = "";
// 初始类型为普通便签
mType = Notes.TYPE_NOTE;
// 初始桌面小组件ID为无效ID没有添加到桌面
mWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
// 初始桌面小组件类型为无效类型
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
// 初始原始所属文件夹ID为0
mOriginParent = 0;
// 初始版本号为0
mVersion = 0;
// 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
// 初始化明细数据列表
mDataList = new ArrayList<SqlData>();
}
/**
* 便
* @param context
* @param c 便
*/
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();
}
/**
* 便ID便
* @param context
* @param id 便ID
*/
public SqlNote(Context context, long id) {
mContext = context;
// 获取数据库操作工具
mContentResolver = context.getContentResolver();
// 标记为已有便签(不是新创建的)
mIsCreate = false;
// 通过便签ID查询数据库再加载便签基本信息
loadFromCursor(id);
// 初始化明细数据列表
mDataList = new ArrayList<SqlData>();
// 如果是普通便签,加载对应的明细内容(比如正文)
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 初始化便签主表的变更记录容器
mDiffNoteValues = new ContentValues();
}
/**
* 便ID便
* @param id 便ID
*/
private void loadFromCursor(long id) {
Cursor c = null; // 数据库查询结果容器
Cursor c = null;
try {
// 根据便签ID查询便签主表获取该便签的基本信息
c = mContentResolver.query(Notes.CONTENT_NOTE_URI, PROJECTION_NOTE, "(_id=?)",
new String[] { String.valueOf(id) }, null);
new String[] {
String.valueOf(id)
}, null);
if (c != null) {
// 移动到查询结果的第一条因为ID是唯一的只有一条结果
c.moveToNext();
// 从查询结果里加载数据到当前对象
loadFromCursor(c);
} else {
// 查询结果为空,打印警告日志
Log.w(TAG, "loadFromCursor: cursor = null");
}
} finally {
// 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
c.close();
}
}
/**
* 便
* 便
* @param c
*/
private void loadFromCursor(Cursor c) {
mId = c.getLong(ID_COLUMN); // 取便签ID
mAlertDate = c.getLong(ALERTED_DATE_COLUMN); // 取提醒时间
mBgColorId = c.getInt(BG_COLOR_ID_COLUMN); // 取背景颜色ID
mCreatedDate = c.getLong(CREATED_DATE_COLUMN); // 取创建时间
mHasAttachment = c.getInt(HAS_ATTACHMENT_COLUMN); // 取是否有附件
mModifiedDate = c.getLong(MODIFIED_DATE_COLUMN); // 取最后修改时间
mParentId = c.getLong(PARENT_ID_COLUMN); // 取所属文件夹ID
mSnippet = c.getString(SNIPPET_COLUMN); // 取便签摘要
mType = c.getInt(TYPE_COLUMN); // 取便签类型
mWidgetId = c.getInt(WIDGET_ID_COLUMN); // 取桌面小组件ID
mWidgetType = c.getInt(WIDGET_TYPE_COLUMN); // 取桌面小组件类型
mVersion = c.getLong(VERSION_COLUMN); // 取便签版本号
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);
}
/**
* 便便
* 便IDmDataList
*/
private void loadDataContent() {
Cursor c = null; // 数据库查询结果容器
// 先清空现有的明细数据列表
Cursor c = null;
mDataList.clear();
try {
// 根据便签ID查询数据明细表获取该便签的明细内容
c = mContentResolver.query(Notes.CONTENT_DATA_URI, SqlData.PROJECTION_DATA,
"(note_id=?)", new String[] { String.valueOf(mId) }, null);
"(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;
}
// 遍历所有明细数据逐个加载到mDataList
while (c.moveToNext()) {
SqlData data = new SqlData(mContext, c); // 从查询结果创建明细数据对象
mDataList.add(data); // 添加到明细数据列表
SqlData data = new SqlData(mContext, c);
mDataList.add(data);
}
} else {
// 查询结果为空,打印警告日志
Log.w(TAG, "loadDataContent: cursor = null");
}
} finally {
// 不管查询成功与否,最后都关闭查询结果,避免占用资源
if (c != null)
c.close();
}
}
/**
* JSON便
* JSON便
*
* @param js 便JSON
* @return true=false=JSON
*/
public boolean setContent(JSONObject js) {
try {
// 从JSON里取出便签基本信息的数据包
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) {
// 从JSON里取文件夹摘要没有的话为空字符串
} 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;
// 从JSON里取文件夹类型没有的话默认是普通便签
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) {
// 取出明细数据的JSON数组比如正文内容
} else if (note.getInt(NoteColumns.TYPE) == Notes.TYPE_NOTE) {
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 处理便签ID
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) {
@ -352,7 +261,6 @@ public class SqlNote {
}
mAlertDate = alertDate;
// 处理背景颜色ID
int bgColorId = note.has(NoteColumns.BG_COLOR_ID) ? note
.getInt(NoteColumns.BG_COLOR_ID) : ResourceParser.getDefaultBgId(mContext);
if (mIsCreate || mBgColorId != bgColorId) {
@ -360,7 +268,6 @@ public class SqlNote {
}
mBgColorId = bgColorId;
// 处理创建时间
long createDate = note.has(NoteColumns.CREATED_DATE) ? note
.getLong(NoteColumns.CREATED_DATE) : System.currentTimeMillis();
if (mIsCreate || mCreatedDate != createDate) {
@ -368,7 +275,6 @@ public class SqlNote {
}
mCreatedDate = createDate;
// 处理是否有附件
int hasAttachment = note.has(NoteColumns.HAS_ATTACHMENT) ? note
.getInt(NoteColumns.HAS_ATTACHMENT) : 0;
if (mIsCreate || mHasAttachment != hasAttachment) {
@ -376,7 +282,6 @@ public class SqlNote {
}
mHasAttachment = hasAttachment;
// 处理最后修改时间
long modifiedDate = note.has(NoteColumns.MODIFIED_DATE) ? note
.getLong(NoteColumns.MODIFIED_DATE) : System.currentTimeMillis();
if (mIsCreate || mModifiedDate != modifiedDate) {
@ -384,7 +289,6 @@ public class SqlNote {
}
mModifiedDate = modifiedDate;
// 处理所属文件夹ID
long parentId = note.has(NoteColumns.PARENT_ID) ? note
.getLong(NoteColumns.PARENT_ID) : 0;
if (mIsCreate || mParentId != parentId) {
@ -392,7 +296,6 @@ public class SqlNote {
}
mParentId = parentId;
// 处理便签摘要
String snippet = note.has(NoteColumns.SNIPPET) ? note
.getString(NoteColumns.SNIPPET) : "";
if (mIsCreate || !mSnippet.equals(snippet)) {
@ -400,7 +303,6 @@ public class SqlNote {
}
mSnippet = snippet;
// 处理便签类型
int type = note.has(NoteColumns.TYPE) ? note.getInt(NoteColumns.TYPE)
: Notes.TYPE_NOTE;
if (mIsCreate || mType != type) {
@ -408,7 +310,6 @@ public class SqlNote {
}
mType = type;
// 处理桌面小组件ID
int widgetId = note.has(NoteColumns.WIDGET_ID) ? note.getInt(NoteColumns.WIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
if (mIsCreate || mWidgetId != widgetId) {
@ -416,7 +317,6 @@ public class SqlNote {
}
mWidgetId = widgetId;
// 处理桌面小组件类型
int widgetType = note.has(NoteColumns.WIDGET_TYPE) ? note
.getInt(NoteColumns.WIDGET_TYPE) : Notes.TYPE_WIDGET_INVALIDE;
if (mIsCreate || mWidgetType != widgetType) {
@ -424,7 +324,6 @@ public class SqlNote {
}
mWidgetType = widgetType;
// 处理原始所属文件夹ID
long originParent = note.has(NoteColumns.ORIGIN_PARENT_ID) ? note
.getLong(NoteColumns.ORIGIN_PARENT_ID) : 0;
if (mIsCreate || mOriginParent != originParent) {
@ -432,12 +331,9 @@ public class SqlNote {
}
mOriginParent = originParent;
// 处理明细数据(比如正文内容)
for (int i = 0; i < dataArray.length(); i++) {
JSONObject data = dataArray.getJSONObject(i);
SqlData sqlData = null;
// 如果明细数据有ID先在现有列表里找对应的明细对象
if (data.has(DataColumns.ID)) {
long dataId = data.getLong(DataColumns.ID);
for (SqlData temp : mDataList) {
@ -447,46 +343,32 @@ public class SqlNote {
}
}
// 如果没找到对应的明细对象,创建新的并加入列表
if (sqlData == null) {
sqlData = new SqlData(mContext);
mDataList.add(sqlData);
}
// 给明细对象设置内容,并记录变更
sqlData.setContent(data);
}
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
}
// 设置成功返回true
return true;
}
/**
* 便JSON
* 便JSON便
* @return 便JSON便/null
*/
public JSONObject getContent() {
try {
// 创建空的JSON数据包
JSONObject js = new JSONObject();
// 如果是新便签还没存到数据库打印错误日志并返回null
if (mIsCreate) {
Log.e(TAG, "it seems that we haven't created this in database yet");
return null;
}
// 创建存储便签基本信息的JSON对象
JSONObject note = new JSONObject();
// 如果是普通便签,打包所有基本信息和明细内容
if (mType == Notes.TYPE_NOTE) {
note.put(NoteColumns.ID, mId);
note.put(NoteColumns.ALERTED_DATE, mAlertDate);
@ -500,174 +382,111 @@ public class SqlNote {
note.put(NoteColumns.WIDGET_ID, mWidgetId);
note.put(NoteColumns.WIDGET_TYPE, mWidgetType);
note.put(NoteColumns.ORIGIN_PARENT_ID, mOriginParent);
// 把便签基本信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
// 创建存储明细数据的JSON数组
JSONArray dataArray = new JSONArray();
for (SqlData sqlData : mDataList) {
// 把每个明细数据打包成JSON
JSONObject data = sqlData.getContent();
if (data != null) {
dataArray.put(data);
}
}
// 把明细数据数组存入总JSON
js.put(GTaskStringUtils.META_HEAD_DATA, dataArray);
}
// 如果是文件夹/系统文件夹只打包ID、类型和摘要
else if (mType == Notes.TYPE_FOLDER || mType == Notes.TYPE_SYSTEM) {
} 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);
// 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, note);
}
// 返回打包好的JSON
return js;
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
// 打包失败返回null
return null;
}
/**
* 便ID
* @param id ID
*/
public void setParentId(long id) {
mParentId = id;
mDiffNoteValues.put(NoteColumns.PARENT_ID, id);
}
/**
* 便GTask ID
* @param gid GTaskID
*/
public void setGtaskId(String gid) {
mDiffNoteValues.put(NoteColumns.GTASK_ID, gid);
}
/**
* 便
* @param syncId
*/
public void setSyncId(long syncId) {
mDiffNoteValues.put(NoteColumns.SYNC_ID, syncId);
}
/**
*
*/
public void resetLocalModified() {
mDiffNoteValues.put(NoteColumns.LOCAL_MODIFIED, 0);
}
/**
* 便ID
* @return 便ID
*/
public long getId() {
return mId;
}
/**
* 便ID
* @return ID
*/
public long getParentId() {
return mParentId;
}
/**
* 便
* @return 便
*/
public String getSnippet() {
return mSnippet;
}
/**
* 便
* @return true=便false=/
*/
public boolean isNoteType() {
return mType == Notes.TYPE_NOTE;
}
/**
* 便
* 便便
* 便
* @param validateVersion 便
*/
public void commit(boolean validateVersion) {
// 如果是新便签(要插入数据库)
if (mIsCreate) {
// 如果便签ID是无效的就把ID从变更记录里移除数据库会自动生成唯一ID
if (mId == INVALID_ID && mDiffNoteValues.containsKey(NoteColumns.ID)) {
mDiffNoteValues.remove(NoteColumns.ID);
}
// 把便签基本信息插入到便签主表返回新便签的URI
Uri uri = mContentResolver.insert(Notes.CONTENT_NOTE_URI, mDiffNoteValues);
try {
// 从返回的URI里提取数据库分配的唯一便签ID
mId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 提取ID失败打印错误日志并抛出异常
Log.e(TAG, "Get note id error :" + e.toString());
throw new ActionFailureException("create note failed");
}
// 如果提取的ID为0说明创建失败抛出异常
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 {
// 验证便签ID是否有效除了根文件夹和通话记录文件夹其他ID不能<=0
} 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 {
+ 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) });
+ NoteColumns.ID + "=?) AND (" + NoteColumns.VERSION + "<=?)",
new String[] {
String.valueOf(mId), String.valueOf(mVersion)
});
}
// 如果更新结果为0说明没有更新成功可能版本不匹配或数据已被修改
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);
@ -675,14 +494,12 @@ public class SqlNote {
}
}
// 刷新便签信息(重新从数据库加载最新数据)
// refresh local info
loadFromCursor(mId);
if (mType == Notes.TYPE_NOTE)
loadDataContent();
// 清空便签主表的变更记录(本次提交完成,下次变更重新记录)
mDiffNoteValues.clear();
// 标记为非新便签(就算是刚插入的,现在也已经存到数据库了)
mIsCreate = false;
}
}
}

@ -32,174 +32,142 @@ import org.json.JSONException;
import org.json.JSONObject;
/**
* GTask便
* GTask便
* Node//
*
*/
public class Task extends Node {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = Task.class.getSimpleName();
// 任务是否完成true=已完成false=未完成)
private boolean mCompleted;
// 任务备注(额外的说明信息)
private String mNotes;
// 本地便签的元信息存储便签的详细配置是个JSON数据包
private JSONObject mMetaInfo;
// 上一个兄弟任务(当前任务在列表里的前一个任务,用来确定任务顺序)
private Task mPriorSibling;
// 所属任务列表(对应本地的文件夹,当前任务在哪个列表/文件夹里)
private TaskList mParent;
/**
*
*
*/
public Task() {
super(); // 调用父类Node的构造方法初始化同步相关的默认属性
mCompleted = false; // 默认任务未完成
mNotes = null; // 默认没有任务备注
mPriorSibling = null; // 默认没有上一个兄弟任务
mParent = null; // 默认没有所属任务列表
mMetaInfo = null; // 默认没有本地便签元信息
super();
mCompleted = false;
mNotes = null;
mPriorSibling = null;
mParent = null;
mMetaInfo = null;
}
/**
*
* 便JSON
* @param actionId
* @return JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“创建任务”
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 2. 设置动作ID给这次创建操作一个唯一编号方便云端识别
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置任务位置:告诉云端这个新任务在所属列表里的排序位置
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mParent.getChildTaskIndex(this));
// 4. 设置任务核心信息:打包任务的名称、创建者等信息
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 任务名称(对应便签摘要/内容)
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID这里默认传null
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) { // 如果有任务备注,就把备注也传过去
GTaskStringUtils.GTASK_JSON_TYPE_TASK);
if (getNotes() != null) {
entity.put(GTaskStringUtils.GTASK_JSON_NOTES, getNotes());
}
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity); // 把任务核心信息存入总JSON
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
// 5. 设置所属列表ID告诉云端这个任务属于哪个列表对应本地文件夹
// parent_id
js.put(GTaskStringUtils.GTASK_JSON_PARENT_ID, mParent.getGid());
// 6. 设置所属列表类型:告诉云端父级是“任务列表”(文件夹类型)
// dest_parent_type
js.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT_TYPE,
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
// 7. 设置列表ID和所属列表ID一致确认任务归属
// list_id
js.put(GTaskStringUtils.GTASK_JSON_LIST_ID, mParent.getGid());
// 8. 设置上一个兄弟任务ID如果有前一个任务就传过去保证任务排序
// prior_sibling_id
if (mPriorSibling != null) {
js.put(GTaskStringUtils.GTASK_JSON_PRIOR_SIBLING_ID, mPriorSibling.getGid());
}
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-create jsonobject");
}
return js; // 返回打包好的创建请求参数
return js;
}
/**
*
* 便JSON
* @param actionId
* @return JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“更新任务”
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 2. 设置动作ID给这次更新操作一个唯一编号方便云端识别
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置任务ID告诉云端要更新哪个任务用云端给的唯一ID
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 4. 设置要更新的任务信息:打包修改后的名称、备注、删除状态
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 更新后的任务名称
if (getNotes() != null) { // 如果有备注,就更新备注
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); // 把更新信息存入总JSON
entity.put(GTaskStringUtils.GTASK_JSON_DELETED, getDeleted());
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate task-update jsonobject");
}
return js; // 返回打包好的更新请求参数
return js;
}
/**
*
*
* @param js JSON
*/
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) { // 如果云端返回的数据不为空
if (js != null) {
try {
// 1. 提取任务ID云端给的唯一编号设置到当前对象
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// 2. 提取任务最后修改时间,设置到当前对象
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 3. 提取任务名称,设置到当前对象
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
// 4. 提取任务备注,设置到当前对象
// notes
if (js.has(GTaskStringUtils.GTASK_JSON_NOTES)) {
setNotes(js.getString(GTaskStringUtils.GTASK_JSON_NOTES));
}
// 5. 提取任务删除状态,设置到当前对象
// deleted
if (js.has(GTaskStringUtils.GTASK_JSON_DELETED)) {
setDeleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_DELETED));
}
// 6. 提取任务完成状态,设置到当前对象
// completed
if (js.has(GTaskStringUtils.GTASK_JSON_COMPLETED)) {
setCompleted(js.getBoolean(GTaskStringUtils.GTASK_JSON_COMPLETED));
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get task content from jsonobject");
@ -207,180 +175,135 @@ public class Task extends Node {
}
}
/**
* 便JSON
* 便JSON便
* @param js 便JSON
*/
public void setContentByLocalJSON(JSONObject js) {
// 如果JSON数据为空或者缺少便签信息/明细内容,打印警告日志
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)
|| !js.has(GTaskStringUtils.META_HEAD_DATA)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
// 1. 提取本地便签的基本信息
JSONObject note = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 2. 提取本地便签的明细内容(比如正文)
JSONArray dataArray = js.getJSONArray(GTaskStringUtils.META_HEAD_DATA);
// 3. 如果不是普通便签类型,打印错误日志并返回
if (note.getInt(NoteColumns.TYPE) != Notes.TYPE_NOTE) {
Log.e(TAG, "invalid type");
return;
}
// 4. 遍历明细内容,提取普通便签的正文作为任务名称
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; // 找到后就停止遍历
setName(data.getString(DataColumns.CONTENT));
break;
}
}
} catch (JSONException e) {
// JSON解析出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* 便JSON
* 便JSON便
* @return 便JSONnull
*/
public JSONObject getLocalJSONFromContent() {
String name = getName(); // 获取任务名称(对应便签内容)
String name = getName();
try {
// 情况1没有本地元信息云端新建的任务要同步到本地
if (mMetaInfo == null) {
// 如果任务名称为空打印警告日志并返回null
// new task created from web
if (name == null) {
Log.w(TAG, "the note seems to be an empty one");
return null;
}
// 创建本地便签的JSON数据包
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; // 返回打包好的本地便签JSON
}
// 情况2有本地元信息已同步过的任务更新本地内容
else {
// 提取已有的本地便签元信息
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; // 找到后停止遍历
data.put(DataColumns.CONTENT, getName());
break;
}
}
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 确保是普通便签类型
return mMetaInfo; // 返回更新后的本地元信息
note.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
return mMetaInfo;
}
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
/**
* 便
* MetaData便JSON
* @param metaData 便
*/
public void setMetaInfo(MetaData metaData) {
// 如果元数据不为空,且包含便签信息
if (metaData != null && metaData.getNotes() != null) {
try {
// 把元数据里的便签信息转换成JSON对象存入mMetaInfo
mMetaInfo = new JSONObject(metaData.getNotes());
} catch (JSONException e) {
// 转换失败,打印警告日志,清空元信息
Log.w(TAG, e.toString());
mMetaInfo = null;
}
}
}
/**
*
* 便
* @param c 便
* @return NodeSYNC_ACTION_XXX
*/
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);
}
// 1. 如果本地元信息为空,说明本地便签已删除,返回“更新云端”(让云端也删除)
if (noteInfo == null) {
Log.w(TAG, "it seems that note meta has been deleted");
return SYNC_ACTION_UPDATE_REMOTE;
}
// 2. 如果元信息里没有便签ID说明本地便签无效返回“更新本地”用云端数据覆盖
if (!noteInfo.has(NoteColumns.ID)) {
Log.w(TAG, "remote note id seems to be deleted");
return SYNC_ACTION_UPDATE_LOCAL;
}
// 3. 验证便签ID是否匹配本地便签ID和元信息里的ID不一致返回“更新本地”
// 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;
}
// 4. 判断本地是否有修改
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 {
// 本地有修改
// 验证云端任务ID是否匹配不匹配返回“同步异常”
// 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();
}
@ -388,79 +311,41 @@ public class Task extends Node {
return SYNC_ACTION_ERROR;
}
/**
*
*
* @return true=//false=
*/
public boolean isWorthSaving() {
return mMetaInfo != null // 有本地元信息
|| (getName() != null && getName().trim().length() > 0) // 任务名称不为空
|| (getNotes() != null && getNotes().trim().length() > 0); // 任务备注不为空
return mMetaInfo != null || (getName() != null && getName().trim().length() > 0)
|| (getNotes() != null && getNotes().trim().length() > 0);
}
/**
*
* @param completed true=false=
*/
public void setCompleted(boolean completed) {
this.mCompleted = completed;
}
/**
*
* @param notes
*/
public void setNotes(String notes) {
this.mNotes = notes;
}
/**
*
* @param priorSibling
*/
public void setPriorSibling(Task priorSibling) {
this.mPriorSibling = priorSibling;
}
/**
*
* @param parent
*/
public void setParent(TaskList parent) {
this.mParent = parent;
}
/**
*
* @return true=false=
*/
public boolean getCompleted() {
return this.mCompleted;
}
/**
*
* @return
*/
public String getNotes() {
return this.mNotes;
}
/**
*
* @return null
*/
public Task getPriorSibling() {
return this.mPriorSibling;
}
/**
*
* @return null
*/
public TaskList getParent() {
return this.mParent;
}
}
}

@ -30,130 +30,98 @@ import org.json.JSONObject;
import java.util.ArrayList;
/**
* GTask
* GTask便
* Node//
* 便
*/
public class TaskList extends Node {
// 日志标签:打印这个类的日志时,用来标识日志来自这个类
private static final String TAG = TaskList.class.getSimpleName();
// 列表排序索引:用来确定这个任务列表在云端的显示顺序
private int mIndex;
// 子任务列表:存储当前列表下的所有任务(对应本地文件夹里的普通便签)
private ArrayList<Task> mChildren;
/**
*
*
*/
public TaskList() {
super(); // 调用父类Node的构造方法初始化同步相关的默认属性
mChildren = new ArrayList<Task>(); // 初始化子任务列表(空列表)
mIndex = 1; // 默认排序索引为1控制列表在云端的显示顺序
super();
mChildren = new ArrayList<Task>();
mIndex = 1;
}
/**
*
* JSON
* @param actionId
* @return JSON
*/
public JSONObject getCreateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“创建任务列表”
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_CREATE);
// 2. 设置动作ID给这次创建操作一个唯一编号方便云端识别
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置列表排序索引:告诉云端这个新列表在云端的显示顺序
// index
js.put(GTaskStringUtils.GTASK_JSON_INDEX, mIndex);
// 4. 设置列表核心信息:打包列表的名称、创建者等信息
// entity_delta
JSONObject entity = new JSONObject();
entity.put(GTaskStringUtils.GTASK_JSON_NAME, getName()); // 列表名称(对应本地文件夹名称)
entity.put(GTaskStringUtils.GTASK_JSON_CREATOR_ID, "null"); // 创建者ID这里默认传null
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); // 把列表核心信息存入总JSON
GTaskStringUtils.GTASK_JSON_TYPE_GROUP);
js.put(GTaskStringUtils.GTASK_JSON_ENTITY_DELTA, entity);
} catch (JSONException e) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-create jsonobject");
}
return js; // 返回打包好的创建请求参数
return js;
}
/**
*
* JSON
* @param actionId
* @return JSON
*/
public JSONObject getUpdateAction(int actionId) {
JSONObject js = new JSONObject(); // 创建空的JSON数据包
JSONObject js = new JSONObject();
try {
// 1. 设置动作类型:告诉云端这次是“更新任务列表”
// action_type
js.put(GTaskStringUtils.GTASK_JSON_ACTION_TYPE,
GTaskStringUtils.GTASK_JSON_ACTION_TYPE_UPDATE);
// 2. 设置动作ID给这次更新操作一个唯一编号方便云端识别
// action_id
js.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, actionId);
// 3. 设置列表ID告诉云端要更新哪个列表用云端给的唯一ID
// id
js.put(GTaskStringUtils.GTASK_JSON_ID, getGid());
// 4. 设置要更新的列表信息:打包修改后的名称、删除状态
// 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); // 把更新信息存入总JSON
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) {
// JSON打包出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to generate tasklist-update jsonobject");
}
return js; // 返回打包好的更新请求参数
return js;
}
/**
*
*
* @param js JSON
*/
public void setContentByRemoteJSON(JSONObject js) {
if (js != null) { // 如果云端返回的数据不为空
if (js != null) {
try {
// 1. 提取列表ID云端给的唯一编号设置到当前对象
// id
if (js.has(GTaskStringUtils.GTASK_JSON_ID)) {
setGid(js.getString(GTaskStringUtils.GTASK_JSON_ID));
}
// 2. 提取列表最后修改时间,设置到当前对象
// last_modified
if (js.has(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED)) {
setLastModified(js.getLong(GTaskStringUtils.GTASK_JSON_LAST_MODIFIED));
}
// 3. 提取列表名称,设置到当前对象
// name
if (js.has(GTaskStringUtils.GTASK_JSON_NAME)) {
setName(js.getString(GTaskStringUtils.GTASK_JSON_NAME));
}
} catch (JSONException e) {
// JSON解析出错打印错误日志并抛出异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("fail to get tasklist content from jsonobject");
@ -161,128 +129,86 @@ public class TaskList extends Node {
}
}
/**
* JSON
* JSON
* @param js JSON
*/
public void setContentByLocalJSON(JSONObject js) {
// 如果JSON数据为空或者缺少文件夹信息打印警告日志
if (js == null || !js.has(GTaskStringUtils.META_HEAD_NOTE)) {
Log.w(TAG, "setContentByLocalJSON: nothing is avaiable");
}
try {
// 1. 提取本地文件夹的基本信息
JSONObject folder = js.getJSONObject(GTaskStringUtils.META_HEAD_NOTE);
// 2. 如果是普通文件夹(用户自己创建的文件夹)
if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_FOLDER) {
// 提取文件夹名称加上MIUI专属前缀区分云端其他列表
String name = folder.getString(NoteColumns.SNIPPET);
setName(GTaskStringUtils.MIUI_FOLDER_PREFFIX + name);
}
// 3. 如果是系统文件夹(自带的根目录/通话记录文件夹)
else if (folder.getInt(NoteColumns.TYPE) == Notes.TYPE_SYSTEM) {
// 根目录文件夹:设置默认名称(加前缀)
} 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");
}
// 4. 无效类型:打印错误日志
else {
} else {
Log.e(TAG, "error type");
}
} catch (JSONException e) {
// JSON解析出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
}
}
/**
* JSON
* JSON便
* @return JSONnull
*/
public JSONObject getLocalJSONFromContent() {
try {
// 创建本地文件夹的JSON数据包
JSONObject js = new JSONObject();
JSONObject folder = new JSONObject(); // 文件夹基本信息
JSONObject folder = new JSONObject();
// 1. 提取云端列表名称去掉MIUI专属前缀还原成本地文件夹名称
String folderName = getName();
if (getName().startsWith(GTaskStringUtils.MIUI_FOLDER_PREFFIX))
folderName = folderName.substring(GTaskStringUtils.MIUI_FOLDER_PREFFIX.length(),
folderName.length());
// 2. 设置文件夹名称(摘要)
folder.put(NoteColumns.SNIPPET, folderName);
// 3. 判断文件夹类型(系统文件夹/普通文件夹)
if (folderName.equals(GTaskStringUtils.FOLDER_DEFAULT)
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE)) {
// 根目录/通话记录文件夹:标记为系统文件夹
|| folderName.equals(GTaskStringUtils.FOLDER_CALL_NOTE))
folder.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
} else {
// 其他文件夹:标记为普通文件夹
else
folder.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
}
// 4. 把文件夹信息存入总JSON
js.put(GTaskStringUtils.META_HEAD_NOTE, folder);
return js; // 返回打包好的本地文件夹JSON
return js;
} catch (JSONException e) {
// JSON打包出错打印错误日志
Log.e(TAG, e.toString());
e.printStackTrace();
return null;
}
}
/**
*
*
* @param c
* @return NodeSYNC_ACTION_XXX
*/
public int getSyncAction(Cursor c) {
try {
// 1. 判断本地文件夹是否有修改
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 {
// 本地有修改
// 2. 验证云端列表ID是否匹配不匹配返回“同步异常”
// 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();
}
@ -290,89 +216,62 @@ public class TaskList extends Node {
return SYNC_ACTION_ERROR;
}
/**
*
* 便
* @return
*/
public int getChildTaskCount() {
return mChildren.size();
}
/**
*
* 便便便
* @param task 便
* @return true=false=/
*/
public boolean addChildTask(Task task) {
boolean ret = false;
// 如果任务不为空,且列表中没有该任务
if (task != null && !mChildren.contains(task)) {
ret = mChildren.add(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); // 设置任务的所属列表(当前文件夹)
task.setParent(this);
}
}
return ret;
}
/**
*
* 便
* @param task 便
* @param index
* @return true=false=/
*/
public boolean addChildTask(Task task, int index) {
// 索引无效小于0或大于列表长度打印错误日志并返回false
if (index < 0 || index > mChildren.size()) {
Log.e(TAG, "add child task: invalid index");
return false;
}
// 查找任务在列表中的位置(-1表示不存在
int pos = mChildren.indexOf(task);
if (task != null && pos == -1) {
mChildren.add(index, task); // 在指定位置插入任务
mChildren.add(index, task);
// 获取该任务的前一个和后一个任务
// update the task list
Task preTask = null;
Task afterTask = null;
if (index != 0)
preTask = mChildren.get(index - 1); // 前一个任务(上一个兄弟)
preTask = mChildren.get(index - 1);
if (index != mChildren.size() - 1)
afterTask = mChildren.get(index + 1); // 后一个任务
afterTask = mChildren.get(index + 1);
task.setPriorSibling(preTask); // 设置当前任务的上一个兄弟
task.setPriorSibling(preTask);
if (afterTask != null)
afterTask.setPriorSibling(task); // 更新后一个任务的上一个兄弟为当前任务
afterTask.setPriorSibling(task);
}
return true;
}
/**
*
* 便便便
* @param task 便
* @return true=false=
*/
public boolean removeChildTask(Task task) {
boolean ret = false;
// 查找任务在列表中的位置
int index = mChildren.indexOf(task);
if (index != -1) {
ret = mChildren.remove(task); // 移除该任务
ret = mChildren.remove(task);
if (ret) {
task.setPriorSibling(null); // 重置该任务的上一个兄弟关系
task.setParent(null); // 重置该任务的所属列表
// 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));
@ -382,40 +281,24 @@ public class TaskList extends Node {
return ret;
}
/**
*
* 便
* @param task 便
* @param index
* @return true=false=//
*/
public boolean moveChildTask(Task task, int index) {
// 索引无效小于0或大于等于列表长度打印错误日志并返回false
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "move child task: invalid index");
return false;
}
// 查找任务当前位置(-1表示不存在
int pos = mChildren.indexOf(task);
if (pos == -1) {
Log.e(TAG, "move child task: the task should in the list");
return false;
}
// 如果当前位置和目标位置一致无需移动返回true
if (pos == index)
return true;
// 先移除任务,再添加到目标位置,返回操作结果
return (removeChildTask(task) && addChildTask(task, index));
}
/**
* ID
* 便ID便
* @param gid ID
* @return 便null
*/
public Task findChildTaskByGid(String gid) {
for (int i = 0; i < mChildren.size(); i++) {
Task t = mChildren.get(i);
@ -426,24 +309,11 @@ public class TaskList extends Node {
return null;
}
/**
*
* 便
* @param task 便
* @return -1
*/
public int getChildTaskIndex(Task task) {
return mChildren.indexOf(task);
}
/**
*
* 便
* @param index
* @return 便null
*/
public Task getChildTaskByIndex(int index) {
// 索引无效打印错误日志并返回null
if (index < 0 || index >= mChildren.size()) {
Log.e(TAG, "getTaskByIndex: invalid index");
return null;
@ -451,12 +321,6 @@ public class TaskList extends Node {
return mChildren.get(index);
}
/**
* IDfindChildTaskByGid
* 便ID便
* @param gid ID
* @return 便null
*/
public Task getChilTaskByGid(String gid) {
for (Task task : mChildren) {
if (task.getGid().equals(gid))
@ -465,30 +329,15 @@ public class TaskList extends Node {
return null;
}
/**
*
* 便
* @return 便
*/
public ArrayList<Task> getChildTaskList() {
return this.mChildren;
}
/**
*
*
* @param index
*/
public void setIndex(int index) {
this.mIndex = index;
}
/**
*
*
* @return
*/
public int getIndex() {
return this.mIndex;
}
}
}

@ -1,27 +1,28 @@
/*
* Description便
* 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;
/*
* serialVersionUIDjava
* serialVersionUID
* Made By Cuican
*/
public ActionFailureException() {
super();
}
/*
* JAVA使superthis.
* new
* 使super
* super()super (paramString)Exception ()Exception (paramString)
* Made By Cuican
*/
public ActionFailureException(String paramString) {
super(paramString);
}
@ -29,4 +30,4 @@ public class ActionFailureException extends RuntimeException {
public ActionFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}
}

@ -1,28 +1,28 @@
/*
* Description便
* 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;
/*
* serialVersionUIDjava
* serialVersionUID
* Made By Cuican
*/
public NetworkFailureException() {
super();
}
/*
* JAVA使superthis.
* new
* 使super
* super()super (paramString)Exception ()Exception (paramString)
* Made By Cuican
*/
public NetworkFailureException(String paramString) {
super(paramString);
}
@ -30,4 +30,4 @@ public class NetworkFailureException extends Exception {
public NetworkFailureException(String paramString, Throwable paramThrowable) {
super(paramString, paramThrowable);
}
}
}

@ -1,3 +1,4 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
@ -16,264 +17,125 @@
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 android.os.Build;
// 导入弱引用工具:防止因一直持有页面/服务导致的内存浪费(通俗说就是不占无用内存)
import java.lang.ref.WeakReference;
// 导入兼容版通知工具:让老安卓手机和新安卓手机都能正常显示通知
import androidx.core.app.NotificationCompat;
// 导入通知权限检查工具用来判断用户有没有开启这个APP的通知权限
import androidx.core.app.NotificationManagerCompat;
// 导入APP资源获取APP里的文字、图标等内容
import net.micode.notes.R;
// 导入便签列表页面:通知点击后跳转到这个页面
import net.micode.notes.ui.NotesListActivity;
// 导入便签设置页面:同步失败时,通知点击后跳转到这个页面
import net.micode.notes.ui.NotesPreferenceActivity;
/**
* GTask便
* "ASync""Async"
*/
// 类名和文件名保持一致GTaskASyncTask建议改成GTaskAsyncTask更规范
public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
// 同步通知的唯一编号:用来区分这个同步通知和其他通知,不会弄混
private static final int GTASK_SYNC_NOTIFICATION_ID = 5234235;
// 通知分类ID安卓8.0以上必须有):给同步通知单独分个类
private static final String GTASK_SYNC_CHANNEL_ID = "gtask_sync_channel";
// 通知分类名称安卓8.0以上必须有):在手机设置里能看到这个分类的名字
private static final String GTASK_SYNC_CHANNEL_NAME = "GTask同步通知";
private static int GTASK_SYNC_NOTIFICATION_ID = 5234235;
/**
*
*
*/
public interface OnCompleteListener {
/**
*
*/
void onComplete();
}
// 用弱引用存上下文(页面/服务信息):页面关闭后,这个引用不会占着内存不放
private final WeakReference<Context> mContextRef;
// 用弱引用存回调监听器:外部页面关闭后,这个监听器不会浪费内存
private final WeakReference<OnCompleteListener> mListenerRef;
// 通知管理器:用来发送、关闭手机通知的工具
private NotificationManager mNotificationManager;
// GTask管理工具专门处理GTask同步逻辑的单例整个APP只有这一个实例
private final GTaskManager mTaskManager;
private Context mContext;
private NotificationManager mNotifiManager;
private GTaskManager mTaskManager;
private OnCompleteListener mOnCompleteListener;
/**
*
* @param context /
* @param listener
*/
public GTaskASyncTask(Context context, OnCompleteListener listener) {
// 把上下文存成弱引用,防止内存浪费
mContextRef = new WeakReference<>(context);
// 把回调监听器存成弱引用,防止内存浪费
mListenerRef = new WeakReference<>(listener);
// 先获取上下文(弱引用要先取出来才能用,还要判断不为空)
Context ctx = mContextRef.get();
if (ctx != null) {
// 获取手机的通知管理服务(用来发通知的)
mNotificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
// 创建通知分类
createNotificationChannel();
}
// 获取GTask管理工具的实例
mContext = context;
mOnCompleteListener = listener;
mNotifiManager = (NotificationManager) mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
mTaskManager = GTaskManager.getInstance();
}
/**
*
*
*/
public void cancelSync() {
mTaskManager.cancelSync();
}
/**
*
* 线
* @param message 2便
*/
public void publishProgress(String message) {
// 调用系统方法,把进度信息传出去
publishProgress(new String[]{message});
public void publishProgess(String message) {
publishProgress(new String[] {
message
});
}
/**
* 8.0
*
*/
private void createNotificationChannel() {
// 先获取上下文
Context ctx = mContextRef.get();
// 上下文为空或者手机系统低于安卓8.0,就不用创建分类
if (ctx == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
// 构建通知分类
android.app.NotificationChannel channel = new android.app.NotificationChannel(
GTASK_SYNC_CHANNEL_ID, // 分类ID
GTASK_SYNC_CHANNEL_NAME, // 分类名称
NotificationManager.IMPORTANCE_DEFAULT // 通知重要性:默认级别(有提示音,不震动)
);
// 开启通知指示灯(如果手机有指示灯的话)
channel.enableLights(true);
// 不在手机桌面APP图标上显示角标
channel.setShowBadge(false);
// 把分类注册到手机系统里
if (mNotificationManager != null) {
mNotificationManager.createNotificationChannel(channel);
}
// 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);
// }
private void showNotification(int tickerId, String content) {
PendingIntent pendingIntent;
if (tickerId != R.string.ticker_success) {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesPreferenceActivity.class), PendingIntent.FLAG_IMMUTABLE);
} else {
pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(mContext,
NotesListActivity.class), PendingIntent.FLAG_IMMUTABLE);
}
/**
*
* //
* @param tickerId IDGTask
* @param content 3便
*/
private void showNotification(int tickerId, String content) {
// 先获取上下文
Context ctx = mContextRef.get();
// 上下文或通知管理器为空,就不显示通知
if (ctx == null || mNotificationManager == null) {
return;
}
// 安卓13以上要检查通知权限用户没开权限就不显示通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (!NotificationManagerCompat.from(ctx).areNotificationsEnabled()) {
// 这里可以提示用户开权限,现在先直接返回
return;
}
}
// 构建通知点击后的跳转意图
PendingIntent pendingIntent;
Intent intent;
// 同步成功就跳转到便签列表,失败/取消就跳转到便签设置
if (tickerId != R.string.ticker_success) {
intent = new Intent(ctx, NotesPreferenceActivity.class); // 跳设置页面
} else {
intent = new Intent(ctx, NotesListActivity.class); // 跳便签列表
}
// 配置跳转意图的标志:更新已有意图,防止重复创建
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
// 安卓6.0以上添加不可变标志,防止报错
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE;
}
// 创建跳转意图
pendingIntent = PendingIntent.getActivity(ctx, 0, intent, flags);
// 构建通知内容
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, GTASK_SYNC_CHANNEL_ID)
.setSmallIcon(R.drawable.notification) // 通知小图标(必须设置,不然不显示)
.setContentTitle(ctx.getString(R.string.app_name)) // 通知标题显示APP名字
.setContentText(content) // 通知正文
.setTicker(ctx.getString(tickerId)) // 通知顶部一闪而过的文字(老手机有效)
.setWhen(System.currentTimeMillis()) // 通知创建时间
.setDefaults(NotificationCompat.DEFAULT_LIGHTS) // 开启默认指示灯
.setAutoCancel(true) // 点击通知后,通知自动消失
.setContentIntent(pendingIntent); // 通知点击后跳转到指定页面
// 显示通知
mNotificationManager.notify(GTASK_SYNC_NOTIFICATION_ID, builder.build());
}
/**
* 线
* @param unused
* @return ///
*/
Notification.Builder builder = new Notification.Builder(mContext)
.setAutoCancel(true)
.setContentTitle(mContext.getString(R.string.app_name))
.setContentText(content)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.setOngoing(true);
Notification notification=builder.getNotification();
mNotifiManager.notify(GTASK_SYNC_NOTIFICATION_ID, notification);
}
@Override
protected Integer doInBackground(Void... unused) {
// 获取上下文
Context ctx = mContextRef.get();
if (ctx != null) {
// 发送进度:提示正在登录同步账号
publishProgress(ctx.getString(R.string.sync_progress_login,
NotesPreferenceActivity.getSyncAccountName(ctx)));
}
// 执行同步逻辑,返回同步结果
return mTaskManager.sync(ctx, this);
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity
.getSyncAccountName(mContext)));
return mTaskManager.sync(mContext, this);
}
/**
* 线/
* @param progress
*/
@Override
protected void onProgressUpdate(String... progress) {
// 显示“正在同步”的通知
showNotification(R.string.ticker_syncing, progress[0]);
// 获取上下文
Context ctx = mContextRef.get();
// 如果上下文是GTask同步服务就发送广播告诉服务当前进度
if (ctx instanceof GTaskSyncService) {
((GTaskSyncService) ctx).sendBroadcast(progress[0]);
if (mContext instanceof GTaskSyncService) {
((GTaskSyncService) mContext).sendBroadcast(progress[0]);
}
}
/**
* 线
* @param result //
*/
@Override
protected void onPostExecute(Integer result) {
// 获取上下文
Context ctx = mContextRef.get();
if (ctx != null) {
// 根据同步结果显示对应通知
if (result == GTaskManager.STATE_SUCCESS) {
// 同步成功:显示成功通知,记录最后同步时间
showNotification(R.string.ticker_success, ctx.getString(
R.string.success_sync_account, mTaskManager.getSyncAccount()));
NotesPreferenceActivity.setLastSyncTime(ctx, System.currentTimeMillis());
} else if (result == GTaskManager.STATE_NETWORK_ERROR) {
// 网络错误:显示网络异常通知
showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_network));
} else if (result == GTaskManager.STATE_INTERNAL_ERROR) {
// 内部错误:显示内部异常通知
showNotification(R.string.ticker_fail, ctx.getString(R.string.error_sync_internal));
} else if (result == GTaskManager.STATE_SYNC_CANCELLED) {
// 同步取消:显示取消通知
showNotification(R.string.ticker_cancel, ctx.getString(R.string.error_sync_cancelled));
}
}
// 获取回调监听器,告诉外部任务完成了
OnCompleteListener listener = mListenerRef.get();
if (listener != null) {
listener.onComplete();
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() {
/**
*
* cancel()
* @param result
*/
@Override
protected void onCancelled(Integer result) {
super.onCancelled(result);
// 告诉外部任务完成了
OnCompleteListener listener = mListenerRef.get();
if (listener != null) {
listener.onComplete();
public void run() {
mOnCompleteListener.onComplete();
}
}).start();
}
}
}
}

@ -16,248 +16,190 @@
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; // 任务数据模型存储单个GTask任务的信息
import net.micode.notes.gtask.data.TaskList; // 任务列表数据模型存储GTask任务列表的信息
import net.micode.notes.gtask.exception.ActionFailureException; // 操作失败异常:创建/删除任务等操作失败时抛出
import net.micode.notes.gtask.exception.NetworkFailureException; // 网络失败异常:网络不通或请求失败时抛出
import net.micode.notes.tool.GTaskStringUtils; // 字符串工具存储GTask接口用到的固定字符串比如接口参数名
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; // GET请求向服务器获取数据的请求方式
import org.apache.http.client.methods.HttpPost; // POST请求向服务器提交数据的请求方式
import org.apache.http.cookie.Cookie; // 登录凭证:用来保持登录状态,不用每次都输账号密码
import org.apache.http.impl.client.BasicCookieStore; // Cookie容器存储登录后的Cookie信息
import org.apache.http.impl.client.DefaultHttpClient; // 网络请求客户端发送GET/POST请求的工具
import org.apache.http.message.BasicNameValuePair; // 键值对:存储请求参数(比如"key=value"
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; // JSON数组存储一组格式化数据比如多个任务信息
import org.json.JSONException; // JSON解析异常数据格式错误导致解析失败时抛出
import org.json.JSONObject; // JSON对象存储键值对格式的数据比如单个任务信息
import java.io.BufferedReader; // 缓冲读取器:高效读取服务器返回的文本内容
import java.io.IOException; // IO异常读取/写入数据失败时抛出
import java.io.InputStream; // 输入流:读取服务器返回的原始数据
import java.io.InputStreamReader; // 输入流读取器:把原始字节数据转换成文本数据
import java.util.LinkedList; // 链表:存储请求参数的容器,有序且方便添加
import java.util.List; // 集合:存储一组数据的通用接口
import java.util.zip.GZIPInputStream; // GZIP解压流解压服务器返回的GZIP格式数据
import java.util.zip.Inflater; // 解压工具处理DEFLATE格式的解压
import java.util.zip.InflaterInputStream; // DEFLATE解压流解压服务器返回的DEFLATE格式数据
/**
* GTask
* GTask
* APP
*/
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();
// GTask基础地址谷歌任务的根地址
private static final String GTASK_URL = "https://mail.google.com/tasks/";
// GTask GET请求地址用来从服务器获取数据比如任务列表
private static final String GTASK_GET_URL = "https://mail.google.com/tasks/ig";
// GTask POST请求地址用来向服务器提交数据比如创建任务、删除任务
private static final String GTASK_POST_URL = "https://mail.google.com/tasks/r/ig";
// 本类的唯一实例保证整个APP只有一个GTaskClient对象
private static GTaskClient mInstance = null;
// 网络请求客户端用来发送GET/POST请求和服务器通信
private DefaultHttpClient mHttpClient;
// 实际使用的GET请求地址可能是默认地址也可能是自定义域名地址
private String mGetUrl;
// 实际使用的POST请求地址可能是默认地址也可能是自定义域名地址
private String mPostUrl;
// 客户端版本号:和服务器交互时需要传递的版本信息,用来兼容不同版本
private long mClientVersion;
// 登录状态标记true=已登录false=未登录
private boolean mLoggedin;
// 上次登录时间:用来判断登录状态是否过期
private long mLastLoginTime;
// 操作ID每次和服务器交互的操作都会分配一个唯一ID自增累加
private int mActionId;
// 同步账号当前用来同步GTask的谷歌账号
private Account mAccount;
// 更新数据数组:存储待提交的更新操作(比如修改任务、新增任务),批量提交更高效
private JSONArray mUpdateArray;
/**
*
* getInstance()
*/
private GTaskClient() {
mHttpClient = null; // 初始化网络请求客户端为null
mGetUrl = GTASK_GET_URL; // 默认使用GTask默认GET地址
mPostUrl = GTASK_POST_URL; // 默认使用GTask默认POST地址
mClientVersion = -1; // 客户端版本号初始化为-1未获取
mLoggedin = false; // 初始状态为未登录
mLastLoginTime = 0; // 上次登录时间初始化为0
mActionId = 1; // 操作ID从1开始自增
mAccount = null; // 同步账号初始化为null
mUpdateArray = null; // 更新数据数组初始化为null
mHttpClient = null;
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
mClientVersion = -1;
mLoggedin = false;
mLastLoginTime = 0;
mActionId = 1;
mAccount = null;
mUpdateArray = null;
}
/**
* GTaskClient
* APPGTaskClient
* @return GTaskClient
*/
public static synchronized GTaskClient getInstance() {
// 如果实例为null就创建一个新的懒加载用到时才创建
if (mInstance == null) {
mInstance = new GTaskClient();
}
return mInstance;
}
/**
* GTask
*
* @param activity
* @return true=false=
*/
public boolean login(Activity activity) {
// 假设登录凭证5分钟过期过期后需要重新登录
// 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; // 登录过期,标记为未登录
mLoggedin = false;
}
// 如果已经登录,但账号和设置里的同步账号不一致(用户切换了账号),也需要重新登录
// need to re-login after account switch
if (mLoggedin
&& !TextUtils.equals(getSyncAccount().name, NotesPreferenceActivity
.getSyncAccountName(activity))) {
.getSyncAccountName(activity))) {
mLoggedin = false;
}
// 如果已经登录且未过期、账号未切换,直接返回登录成功
if (mLoggedin) {
Log.d(TAG, "already logged in"); // 打印日志:已登录
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"); // 打印日志:谷歌账号登录失败
Log.e(TAG, "login google account failed");
return false;
}
// 处理非gmail/googlemail域名的账号自定义域名账号需要切换请求地址
// 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; // 找到@符号的位置,截取域名后缀
int index = mAccount.name.indexOf('@') + 1;
String suffix = mAccount.name.substring(index);
url.append(suffix + "/");
mGetUrl = url.toString() + "ig"; // 自定义GET地址
mPostUrl = url.toString() + "r/ig"; // 自定义POST地址
mGetUrl = url.toString() + "ig";
mPostUrl = url.toString() + "r/ig";
// 使用自定义地址尝试登录GTask
if (tryToLoginGtask(activity, authToken)) {
mLoggedin = true; // 登录成功,标记为已登录
mLoggedin = true;
}
}
// 如果自定义地址登录失败,使用默认地址再次尝试登录
// try to login with google official url
if (!mLoggedin) {
mGetUrl = GTASK_GET_URL; // 恢复默认GET地址
mPostUrl = GTASK_POST_URL; // 恢复默认POST地址
// 默认地址登录失败返回false
mGetUrl = GTASK_GET_URL;
mPostUrl = GTASK_POST_URL;
if (!tryToLoginGtask(activity, authToken)) {
return false;
}
}
// 所有登录逻辑完成,标记为已登录
mLoggedin = true;
return true;
}
/**
*
* GTask
* @param activity
* @param invalidateToken true
* @return null=
*/
private String loginGoogleAccount(Activity activity, boolean invalidateToken) {
String authToken; // 登录凭证
// 获取手机的账号管理器
String authToken;
AccountManager accountManager = AccountManager.get(activity);
// 获取手机里所有的谷歌账号
Account[] accounts = accountManager.getAccountsByType("com.google");
// 如果没有谷歌账号返回null
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 {
// 没找到匹配的账号打印日志并返回null
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) {
// 获取凭证失败打印日志并设为null
Log.e(TAG, "get auth token failed");
authToken = null;
}
@ -265,24 +207,16 @@ public class GTaskClient {
return authToken;
}
/**
* GTask
*
* @param activity
* @param authToken
* @return true=false=
*/
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;
}
// 用新凭证再次尝试登录还是失败就返回false
if (!loginGtask(authToken)) {
Log.e(TAG, "login gtask failed");
return false;
@ -291,76 +225,54 @@ public class GTaskClient {
return true;
}
/**
* GTask
* Cookie
* @param authToken
* @return true=false=
*/
private boolean loginGtask(String authToken) {
// 设置网络连接超时时间10秒连接不上服务器就超时
int timeoutConnection = 10000;
// 设置网络读取超时时间15秒服务器没返回数据就超时
int timeoutSocket = 15000;
// 创建网络参数容器
HttpParams httpParameters = new BasicHttpParams();
// 设置连接超时时间
HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
// 设置读取超时时间
HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
// 初始化网络请求客户端,传入超时参数
mHttpClient = new DefaultHttpClient(httpParameters);
// 创建Cookie容器存储登录后的凭证
BasicCookieStore localBasicCookieStore = new BasicCookieStore();
// 给网络客户端设置Cookie容器
mHttpClient.setCookieStore(localBasicCookieStore);
// 关闭Expect-Continue协议避免部分服务器不兼容
HttpProtocolParams.setUseExpectContinue(mHttpClient.getParams(), false);
// 执行GTask登录请求
// login gtask
try {
// 拼接登录请求地址(带登录凭证)
String loginUrl = mGetUrl + "?auth=" + authToken;
// 创建GET请求
HttpGet httpGet = new HttpGet(loginUrl);
HttpResponse response = null;
// 发送请求,获取服务器响应
response = mHttpClient.execute(httpGet);
// 获取登录后的Cookie判断是否包含GTask的授权Cookie
// get the cookie now
List<Cookie> cookies = mHttpClient.getCookieStore().getCookies();
boolean hasAuthCookie = false;
for (Cookie cookie : cookies) {
if (cookie.getName().contains("GTL")) {
hasAuthCookie = true; // 包含授权Cookie
hasAuthCookie = true;
}
}
if (!hasAuthCookie) {
Log.w(TAG, "it seems that there is no auth cookie"); // 打印警告没有授权Cookie
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>";
// 找到JSON数据的起始和结束位置
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
// 截取JSON数据字符串
jsString = resString.substring(begin + jsBegin.length(), end);
}
// 解析JSON数据获取客户端版本号
JSONObject js = new JSONObject(jsString);
mClientVersion = js.getLong("v");
} catch (JSONException e) {
// JSON解析失败打印日志并返回false
Log.e(TAG, e.toString());
e.printStackTrace();
return false;
} catch (Exception e) {
// 其他异常比如网络异常打印日志并返回false
// simply catch all exceptions
Log.e(TAG, "httpget gtask_url failed");
return false;
}
@ -368,230 +280,152 @@ public class GTaskClient {
return true;
}
/**
* ID
* ID123...
* @return ID
*/
private int getActionId() {
return mActionId++;
}
/**
* POST
* POST
* @return POST
*/
private HttpPost createHttpPost() {
HttpPost httpPost = new HttpPost(mPostUrl);
// 设置请求内容类型表单格式编码为UTF-8
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
// 设置AT请求头固定值1GTask接口要求
httpPost.setHeader("AT", "1");
return httpPost;
}
/**
*
* GZIP/DEFLATE
* @param entity
* @return
* @throws IOException
*/
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();
// 如果是GZIP压缩格式解压后再读取
if (contentEncoding != null && contentEncoding.equalsIgnoreCase("gzip")) {
input = new GZIPInputStream(entity.getContent());
}
// 如果是DEFLATE压缩格式解压后再读取
else if (contentEncoding != null && contentEncoding.equalsIgnoreCase("deflate")) {
} 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(); // 存储读取到的文本内容
StringBuilder sb = new StringBuilder();
// 循环读取每一行文本,直到读取完毕
while (true) {
String buff = br.readLine();
if (buff == null) {
return sb.toString(); // 返回读取到的所有文本
return sb.toString();
}
sb = sb.append(buff); // 把每行文本添加到字符串中
sb = sb.append(buff);
}
} finally {
input.close(); // 关闭输入流,释放资源
input.close();
}
}
/**
* POSTGTask
* JSON
* @param js JSON
* @return JSON
* @throws NetworkFailureException
*/
private JSONObject postRequest(JSONObject js) throws NetworkFailureException {
// 如果未登录,抛出异常
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
// 创建配置好的POST请求
HttpPost httpPost = createHttpPost();
try {
// 创建请求参数列表(存储"r=JSON字符串"这个参数)
LinkedList<BasicNameValuePair> list = new LinkedList<BasicNameValuePair>();
list.add(new BasicNameValuePair("r", js.toString()));
// 把参数列表封装成表单实体
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(list, "UTF-8");
// 给POST请求设置请求实体
httpPost.setEntity(entity);
// 发送POST请求获取服务器响应
// execute the post
HttpResponse response = mHttpClient.execute(httpPost);
// 获取响应的文本内容
String jsString = getResponseContent(response.getEntity());
// 把文本内容解析成JSON对象返回
return new JSONObject(jsString);
} catch (ClientProtocolException e) {
// 客户端协议异常,打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (IOException e) {
// IO异常比如网络断开打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("postRequest failed");
} catch (JSONException e) {
// JSON解析异常打印日志并抛出操作失败异常
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");
}
}
/**
* GTask
* GTaskID
* @param task
* @throws NetworkFailureException
*/
public void createTask(Task task) throws NetworkFailureException {
// 先提交之前待处理的更新操作
commitUpdate();
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务的操作)
JSONArray actionList = new JSONArray();
// 把创建任务的操作添加到操作列表
// action_list
actionList.put(task.getCreateAction(getActionId()));
// 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 给JSON数据添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求获取服务器响应
// post
JSONObject jsResponse = postRequest(jsPost);
// 从响应中获取创建任务的结果
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
// 把服务器返回的任务ID设置到本地任务对象中
task.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create task: handing jsonobject failed");
}
}
/**
* GTask
* GTaskID
* @param tasklist
* @throws NetworkFailureException
*/
public void createTaskList(TaskList tasklist) throws NetworkFailureException {
// 先提交之前待处理的更新操作
commitUpdate();
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray(); // 操作列表(存储创建任务列表的操作)
JSONArray actionList = new JSONArray();
// 把创建任务列表的操作添加到操作列表
// action_list
actionList.put(tasklist.getCreateAction(getActionId()));
// 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 给JSON数据添加客户端版本号
// client version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求获取服务器响应
// post
JSONObject jsResponse = postRequest(jsPost);
// 从响应中获取创建任务列表的结果
JSONObject jsResult = (JSONObject) jsResponse.getJSONArray(
GTaskStringUtils.GTASK_JSON_RESULTS).get(0);
// 把服务器返回的列表ID设置到本地任务列表对象中
tasklist.setGid(jsResult.getString(GTaskStringUtils.GTASK_JSON_NEW_ID));
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("create tasklist: handing jsonobject failed");
}
}
/**
*
* mUpdateArray/
* @throws NetworkFailureException
*/
public void commitUpdate() throws NetworkFailureException {
// 如果有未提交的更新操作
if (mUpdateArray != null) {
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
// 给JSON数据添加更新操作列表
// action_list
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, mUpdateArray);
// 给JSON数据添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求提交更新
postRequest(jsPost);
mUpdateArray = null; // 提交后清空更新列表
mUpdateArray = null;
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("commit update: handing jsonobject failed");
@ -599,230 +433,153 @@ public class GTaskClient {
}
}
/**
* /
* 10
* @param node /
* @throws NetworkFailureException
*/
public void addUpdateNode(Node node) throws NetworkFailureException {
if (node != null) {
// 如果更新列表不为空且数量超过10个先提交一次
// too many update items may result in an error
// set max to 10 items
if (mUpdateArray != null && mUpdateArray.length() > 10) {
commitUpdate();
}
// 如果更新列表为空初始化一个新的JSON数组
if (mUpdateArray == null)
mUpdateArray = new JSONArray();
// 把节点的更新操作添加到更新列表
mUpdateArray.put(node.getUpdateAction(getActionId()));
}
}
/**
* GTask
*
* @param task
* @param preParent
* @param curParent
* @throws NetworkFailureException
*/
public void moveTask(Task task, TaskList preParent, TaskList curParent)
throws NetworkFailureException {
// 先提交之前待处理的更新操作
commitUpdate();
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray(); // 操作列表
JSONObject action = 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);
// 设置操作ID
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
// 设置要移动的任务ID
action.put(GTaskStringUtils.GTASK_JSON_ID, task.getGid());
// 如果在同一个列表内移动且不是第一个任务设置上一个任务的ID用来确定位置
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());
}
// 设置任务原来的列表ID
action.put(GTaskStringUtils.GTASK_JSON_SOURCE_LIST, preParent.getGid());
// 设置任务目标列表的父ID
action.put(GTaskStringUtils.GTASK_JSON_DEST_PARENT, curParent.getGid());
// 如果在不同列表间移动设置目标列表ID
if (preParent != curParent) {
// put the dest_list only if moving between tasklists
action.put(GTaskStringUtils.GTASK_JSON_DEST_LIST, curParent.getGid());
}
// 把移动操作添加到操作列表
actionList.put(action);
// 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 给JSON数据添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求执行移动操作
postRequest(jsPost);
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("move task: handing jsonobject failed");
}
}
/**
* GTask/
*
* @param node /
* @throws NetworkFailureException
*/
public void deleteNode(Node node) throws NetworkFailureException {
// 先提交之前待处理的更新操作
commitUpdate();
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray(); // 操作列表
JSONArray actionList = new JSONArray();
// 把节点标记为已删除
// action_list
node.setDeleted(true);
// 把删除操作添加到操作列表
actionList.put(node.getUpdateAction(getActionId()));
// 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 给JSON数据添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求执行删除操作
postRequest(jsPost);
mUpdateArray = null; // 清空更新列表
mUpdateArray = null;
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("delete node: handing jsonobject failed");
}
}
/**
* GTask
* GTask
* @return JSON
* @throws NetworkFailureException
*/
public JSONArray getTaskLists() throws NetworkFailureException {
// 如果未登录,抛出异常
if (!mLoggedin) {
Log.e(TAG, "please login first");
throw new ActionFailureException("not logged in");
}
try {
// 创建GET请求
HttpGet httpGet = new HttpGet(mGetUrl);
HttpResponse response = null;
// 发送GET请求获取服务器响应
response = mHttpClient.execute(httpGet);
// 获取响应的文本内容
// get the task list
String resString = getResponseContent(response.getEntity());
String jsBegin = "_setup(";
String jsEnd = ")}</script>";
// 找到JSON数据的起始和结束位置
int begin = resString.indexOf(jsBegin);
int end = resString.lastIndexOf(jsEnd);
String jsString = null;
if (begin != -1 && end != -1 && begin < end) {
// 截取JSON数据字符串
jsString = resString.substring(begin + jsBegin.length(), end);
}
// 解析JSON数据获取任务列表数组
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) {
// IO异常打印日志并抛出网络失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new NetworkFailureException("gettasklists: httpget failed");
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task lists: handing jasonobject failed");
}
}
/**
* GTask
* ID
* @param listGid ID
* @return JSON
* @throws NetworkFailureException
*/
public JSONArray getTaskList(String listGid) throws NetworkFailureException {
// 先提交之前待处理的更新操作
commitUpdate();
try {
// 创建POST请求的JSON数据
JSONObject jsPost = new JSONObject();
JSONArray actionList = new JSONArray(); // 操作列表
JSONObject action = 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);
// 设置操作ID
action.put(GTaskStringUtils.GTASK_JSON_ACTION_ID, getActionId());
// 设置要获取的任务列表ID
action.put(GTaskStringUtils.GTASK_JSON_LIST_ID, listGid);
// 设置是否获取已删除的任务false=不获取
action.put(GTaskStringUtils.GTASK_JSON_GET_DELETED, false);
// 把获取操作添加到操作列表
actionList.put(action);
// 给JSON数据添加操作列表
jsPost.put(GTaskStringUtils.GTASK_JSON_ACTION_LIST, actionList);
// 给JSON数据添加客户端版本号
// client_version
jsPost.put(GTaskStringUtils.GTASK_JSON_CLIENT_VERSION, mClientVersion);
// 发送POST请求获取服务器响应
JSONObject jsResponse = postRequest(jsPost);
// 从响应中获取任务数组并返回
return jsResponse.getJSONArray(GTaskStringUtils.GTASK_JSON_TASKS);
} catch (JSONException e) {
// JSON解析失败打印日志并抛出操作失败异常
Log.e(TAG, e.toString());
e.printStackTrace();
throw new ActionFailureException("get task list: handing jsonobject failed");
}
}
/**
*
* GTask
* @return
*/
public Account getSyncAccount() {
return mAccount;
}
/**
*
*
*/
public void resetUpdateArray() {
mUpdateArray = null;
}
}
}

@ -16,206 +16,113 @@
package net.micode.notes.gtask.remote;
import android.app.Activity; // 页面类:外部启动同步时需要关联的页面
import android.app.Service; // 后台服务类:在后台执行耗时操作(同步),不占用前台页面资源
import android.content.Context; // 上下文:保存页面/服务信息,用来启动服务
import android.content.Intent; // 意图:用来传递指令(启动/取消同步)、发送广播
import android.os.Bundle; // 数据容器:用来存储意图中的额外参数(比如同步操作类型)
import android.os.IBinder; // 绑定接口:服务绑定相关(这里用不到)
/**
* GTask
* GTask
* 广
* 使
*/
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";
// 静态变量当前正在执行的同步异步任务整个APP共享确保只有一个同步任务在运行
private static GTaskASyncTask mSyncTask = null;
// 静态变量:当前同步进度提示文字(比如“正在初始化列表”“正在同步”)
private static String mSyncProgress = "";
/**
*
*
*/
private void startSync() {
// 如果当前没有正在执行的同步任务,才创建新任务(避免重复同步)
if (mSyncTask == null) {
// 创建GTask同步异步任务传入服务上下文和任务完成监听器
mSyncTask = new GTaskASyncTask(this, new GTaskASyncTask.OnCompleteListener() {
/**
*
*
*/
public void onComplete() {
mSyncTask = null; // 同步完成,清空当前任务引用
sendBroadcast(""); // 发送空进度广播,通知界面同步已结束
stopSelf(); // 停止当前服务(同步完成,不需要后台服务了)
mSyncTask = null;
sendBroadcast("");
stopSelf();
}
});
sendBroadcast(""); // 发送广播,通知界面同步已开始
mSyncTask.execute(); // 执行同步异步任务(在后台开始同步)
sendBroadcast("");
mSyncTask.execute();
}
}
/**
*
*
*/
private void cancelSync() {
// 如果当前有正在执行的同步任务
if (mSyncTask != null) {
// 调用异步任务的取消方法,终止同步
mSyncTask.cancelSync();
}
}
/**
*
* null
*/
@Override
public void onCreate() {
mSyncTask = null; // 初始化当前同步任务为null还没有同步任务执行
mSyncTask = null;
}
/**
*
*
* @param intent
* @param flags 使
* @param startId ID使
* @return START_STICKY=
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 获取意图中的额外参数(存储了同步操作类型)
Bundle bundle = intent.getExtras();
// 如果参数不为空,且包含“同步操作类型”的键
if (bundle != null && bundle.containsKey(ACTION_STRING_NAME)) {
// 根据同步操作类型执行对应逻辑
switch (bundle.getInt(ACTION_STRING_NAME, ACTION_INVALID)) {
case ACTION_START_SYNC:
// 收到“启动同步”指令,执行启动同步方法
startSync();
break;
case ACTION_CANCEL_SYNC:
// 收到“取消同步”指令,执行取消同步方法
cancelSync();
break;
default:
// 收到无效指令,不做处理
break;
}
// 返回START_STICKY服务被意外杀死比如内存不足系统会尝试重启服务
return START_STICKY;
}
// 如果没有有效参数,执行父类默认逻辑
return super.onStartCommand(intent, flags, startId);
}
/**
*
*
*/
@Override
public void onLowMemory() {
// 如果有正在执行的同步任务,就取消它,释放内存
if (mSyncTask != null) {
mSyncTask.cancelSync();
}
}
/**
*
* null
* @param intent
* @return null
*/
public IBinder onBind(Intent intent) {
return null;
}
/**
* 广
* 广
* @param msg
*/
public void sendBroadcast(String msg) {
mSyncProgress = 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);
}
/**
* GTask
* GTask
* @param activity
*/
public static void startSync(Activity activity) {
// 给GTask管理器设置关联页面用于登录
GTaskManager.getInstance().setActivityContext(activity);
// 创建启动服务的意图,指定服务类
Intent intent = new Intent(activity, GTaskSyncService.class);
// 存入“启动同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_START_SYNC);
// 启动后台服务(开始同步)
activity.startService(intent);
}
/**
* GTask
*
* @param context
*/
public static void cancelSync(Context context) {
// 创建启动服务的意图,指定服务类
Intent intent = new Intent(context, GTaskSyncService.class);
// 存入“取消同步”的操作类型
intent.putExtra(GTaskSyncService.ACTION_STRING_NAME, GTaskSyncService.ACTION_CANCEL_SYNC);
// 启动后台服务(执行取消同步逻辑)
context.startService(intent);
}
/**
*
*
* @return true=false=
*/
public static boolean isSyncing() {
return mSyncTask != null; // 有同步任务就是正在同步
return mSyncTask != null;
}
/**
*
*
* @return
*/
public static String getProgressString() {
return mSyncProgress;
}
}
}

@ -15,239 +15,152 @@
*/
package net.micode.notes.model;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.UserManager;
import java.util.ArrayList;
import android.content.ContentProviderOperation; // 数据库批量操作指令:用来批量执行数据库更新操作
import android.content.ContentProviderResult; // 数据库批量操作结果:存储批量操作的执行结果
import android.content.ContentUris; // URI拼接工具用来拼接便签/数据的数据库访问地址
import android.content.ContentValues; // 数据键值对容器:用来存储要写入数据库的字段和对应值
import android.content.Context; // 上下文:用来获取本地数据库操作工具
import android.content.OperationApplicationException; // 批量操作异常:批量执行数据库操作失败时抛出
import android.net.Uri; // 数据库访问地址:标识数据库中的某类数据(比如便签表、便签数据表)
import android.os.RemoteException; // 远程操作异常:跨进程访问数据库失败时抛出
import android.util.Log; // 日志工具:打印错误信息,方便排查问题
import net.micode.notes.data.Notes; // 便签常量类:存储便签类型、数据库地址等固定值
import net.micode.notes.data.Notes.CallNote; // 通话便签常量存储通话便签的MIME类型等
import net.micode.notes.data.Notes.DataColumns; // 便签数据列便签数据表的字段名比如所属便签ID、内容
import net.micode.notes.data.Notes.NoteColumns; // 便签主列:便签主表的字段名(比如创建时间、修改时间)
import net.micode.notes.data.Notes.TextNote; // 文本便签常量存储文本便签的MIME类型等
import java.util.ArrayList; // 数组列表:用来存储批量操作指令
/**
* 便
* 便便ID便
* 便
*/
public class Note {
// 便签主信息修改容器:存储便签主表的修改字段(比如修改时间、本地修改标记)
private ContentValues mNoteDiffValues;
// 便签具体数据管理对象:负责存储和管理便签的文本/通话记录数据
private NoteData mNoteData;
// 日志标签:打印日志时标识是这个类的日志,方便查找问题
private static final String TAG = "Note";
/**
* 便ID便
* 便便IDID便
* @param context
* @param folderId 便ID
* @return 便ID0
* Create a new note id for adding a new note to databases
*/
public static synchronized long getNewNoteId(Context context, long folderId) {
// 1. 准备新便签的默认数据(存储到便签主表)
// Create a new note in the database
ContentValues values = new ContentValues();
long createdTime = System.currentTimeMillis(); // 获取当前时间作为创建时间
values.put(NoteColumns.CREATED_DATE, createdTime); // 存入创建时间
values.put(NoteColumns.MODIFIED_DATE, createdTime); // 存入修改时间(初始和创建时间一致)
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); // 存入便签类型(普通文本便签)
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 存入本地修改标记1=已修改,需要同步)
values.put(NoteColumns.PARENT_ID, folderId); // 存入所属文件夹ID
// 2. 插入新便签到数据库获取数据库返回的访问地址包含新便签ID
long createdTime = System.currentTimeMillis();
values.put(NoteColumns.CREATED_DATE, createdTime);
values.put(NoteColumns.MODIFIED_DATE, createdTime);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.PARENT_ID, folderId);
// 设置当前用户ID
long currentUserId = UserManager.getInstance(context).getCurrentUserId();
values.put(NoteColumns.USER_ID, currentUserId);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
// 3. 从访问地址中解析出新便签ID
long noteId = 0;
try {
// 数据库返回的Uri格式是content://xxx/notes/[noteId]取第2个分段就是便签ID
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
// 解析ID失败打印错误日志
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
if (uri != null) {
try {
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get note id error :" + e.toString());
noteId = 0;
}
}
// 4. 校验便签ID是否有效-1表示插入失败
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);
}
return noteId;
}
/**
* 便
* Note便
*/
public Note() {
mNoteDiffValues = new ContentValues(); // 初始化便签主信息修改容器
mNoteData = new NoteData(); // 初始化便签具体数据管理对象
mNoteDiffValues = new ContentValues();
mNoteData = new NoteData();
}
/**
* 便
* 便
* @param key 便NoteColumns.TITLE
* @param value 便
*/
public void setNoteValue(String key, String value) {
mNoteDiffValues.put(key, value); // 存入要修改的字段和值
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改需要同步到GTask
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); // 更新修改时间为当前时间
mNoteDiffValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
* 便
* 便
* @param key 便TextNote.CONTENT
* @param value 便
*/
public void setTextData(String key, String value) {
mNoteData.setTextData(key, value); // 委托给NoteData对象处理
mNoteData.setTextData(key, value);
}
/**
* ID
* 便IDID
* @param id ID
*/
public void setTextDataId(long id) {
mNoteData.setTextDataId(id); // 委托给NoteData对象处理
mNoteData.setTextDataId(id);
}
/**
* ID
* 便ID
* @return ID
*/
public long getTextDataId() {
return mNoteData.mTextDataId; // 从NoteData对象中获取
return mNoteData.mTextDataId;
}
/**
* ID
* 便IDID
* @param id ID
*/
public void setCallDataId(long id) {
mNoteData.setCallDataId(id); // 委托给NoteData对象处理
mNoteData.setCallDataId(id);
}
/**
* 便
* 便
* @param key 便CallNote.NUMBER
* @param value 10086
*/
public void setCallData(String key, String value) {
mNoteData.setCallData(key, value); // 委托给NoteData对象处理
mNoteData.setCallData(key, value);
}
/**
* 便
* 便/GTask
* @return true=false=
*/
public boolean isLocalModified() {
// 主信息容器有数据 或 具体数据有修改,就表示便签有本地修改
return mNoteDiffValues.size() > 0 || mNoteData.isLocalModified();
}
/**
* 便
* 便
* @param context
* @param noteId 便ID
* @return true=false=
*/
public boolean syncNote(Context context, long noteId) {
// 1. 校验便签ID是否有效必须大于0
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 2. 如果没有本地修改,直接返回成功(无需同步)
if (!isLocalModified()) {
return true;
}
/**
* 便
* 使便便
* In theory, once data changed, the note should be updated on {@link NoteColumns#LOCAL_MODIFIED} and
* {@link NoteColumns#MODIFIED_DATE}. For data safety, though update note fails, we also update the
* note data info
*/
// 3. 更新便签主信息到数据库
if (context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), // 拼接该便签的数据库访问地址
mNoteDiffValues, // 要更新的主信息数据
null, null) == 0) {
// 更新返回0表示失败打印错误日志理论上不该发生
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), mNoteDiffValues, null,
null) == 0) {
Log.e(TAG, "Update note error, should not happen");
// 不返回,继续尝试更新具体数据
// Do not return, fall through
}
mNoteDiffValues.clear(); // 主信息更新完成(无论成败),清空容器
mNoteDiffValues.clear();
// 4. 更新便签具体数据到数据库
if (mNoteData.isLocalModified()
&& (mNoteData.pushIntoContentResolver(context, noteId) == null)) {
// 具体数据有修改 且 更新失败,返回同步失败
return false;
}
// 5. 所有修改同步完成,返回成功
return true;
}
/**
* 便
* 便
*/
private class NoteData {
// 文本数据ID对应数据库里文本便签数据的唯一ID
private long mTextDataId;
// 文本数据修改容器:存储文本数据的修改字段和值
private ContentValues mTextDataValues;
// 通话数据ID对应数据库里通话便签数据的唯一ID
private long mCallDataId;
// 通话数据修改容器:存储通话数据的修改字段和值
private ContentValues mCallDataValues;
// 日志标签:打印该内部类的错误日志
private static final String TAG = "NoteData";
/**
* 便
*/
public NoteData() {
mTextDataValues = new ContentValues(); // 初始化文本数据容器
mCallDataValues = new ContentValues(); // 初始化通话数据容器
mTextDataId = 0; // 初始文本数据ID为0未关联数据库记录
mCallDataId = 0; // 初始通话数据ID为0未关联数据库记录
mTextDataValues = new ContentValues();
mCallDataValues = new ContentValues();
mTextDataId = 0;
mCallDataId = 0;
}
/**
*
*
* @return true=false=
*/
boolean isLocalModified() {
// 文本数据容器有数据 或 通话数据容器有数据,就表示有修改
return mTextDataValues.size() > 0 || mCallDataValues.size() > 0;
}
/**
* ID
* IDID0
* @param id ID
*/
void setTextDataId(long id) {
if(id <= 0) {
throw new IllegalArgumentException("Text data id should larger than 0");
@ -255,11 +168,6 @@ public class Note {
mTextDataId = id;
}
/**
* ID
* IDID0
* @param id ID
*/
void setCallDataId(long id) {
if (id <= 0) {
throw new IllegalArgumentException("Call data id should larger than 0");
@ -267,122 +175,88 @@ public class Note {
mCallDataId = id;
}
/**
*
* 便
* @param key CallNote.DATE
* @param value
*/
void setCallData(String key, String value) {
mCallDataValues.put(key, value); // 存入通话数据修改
// 同步更新便签主信息的“本地修改标记”和“修改时间”
mCallDataValues.put(key, value);
mNoteDiffValues.put(NoteColumns.LOCAL_MODIFIED, 1);
mNoteDiffValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
}
/**
*
* 便
* @param key TextNote.CONTENT
* @param value 便
*/
void setTextData(String key, String value) {
mTextDataValues.put(key, 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
*/
Uri pushIntoContentResolver(Context context, long noteId) {
// 1. 校验便签ID是否有效
/**
* Check for safety
*/
if (noteId <= 0) {
throw new IllegalArgumentException("Wrong note id:" + noteId);
}
// 2. 准备数据库批量操作指令列表(批量执行更新,效率更高)
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
ContentProviderOperation.Builder builder = null; // 批量操作指令构建器
ContentProviderOperation.Builder builder = null;
// 3. 处理文本数据同步
if(mTextDataValues.size() > 0) {
mTextDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
mTextDataValues.put(DataColumns.NOTE_ID, noteId);
if (mTextDataId == 0) {
// 文本数据ID为0表示是新增文本数据插入到数据库
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE); // 标记为文本数据类型
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mTextDataValues);
mTextDataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mTextDataValues);
try {
// 解析插入后返回的ID绑定到文本数据
setTextDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
// 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new text data fail with noteId" + noteId);
mTextDataValues.clear();
return null;
}
} else {
// 文本数据ID不为0表示是更新已有文本数据添加批量更新指令
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mTextDataId)); // 拼接文本数据的数据库地址
builder.withValues(mTextDataValues); // 设置要更新的数据
operationList.add(builder.build()); // 添加到批量操作列表
Notes.CONTENT_DATA_URI, mTextDataId));
builder.withValues(mTextDataValues);
operationList.add(builder.build());
}
mTextDataValues.clear(); // 文本数据处理完成,清空容器
mTextDataValues.clear();
}
// 4. 处理通话数据同步(逻辑和文本数据一致)
if(mCallDataValues.size() > 0) {
mCallDataValues.put(DataColumns.NOTE_ID, noteId); // 绑定所属便签ID
mCallDataValues.put(DataColumns.NOTE_ID, noteId);
if (mCallDataId == 0) {
// 通话数据ID为0新增通话数据插入到数据库
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE); // 标记为通话数据类型
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI, mCallDataValues);
mCallDataValues.put(DataColumns.MIME_TYPE, CallNote.CONTENT_ITEM_TYPE);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_DATA_URI,
mCallDataValues);
try {
// 解析插入后返回的ID绑定到通话数据
setCallDataId(Long.valueOf(uri.getPathSegments().get(1)));
} catch (NumberFormatException e) {
// 插入失败,打印日志并清空数据容器
Log.e(TAG, "Insert new call data fail with noteId" + noteId);
mCallDataValues.clear();
return null;
}
} else {
// 通话数据ID不为0更新已有通话数据添加批量更新指令
builder = ContentProviderOperation.newUpdate(ContentUris.withAppendedId(
Notes.CONTENT_DATA_URI, mCallDataId)); // 拼接通话数据的数据库地址
builder.withValues(mCallDataValues); // 设置要更新的数据
operationList.add(builder.build()); // 添加到批量操作列表
Notes.CONTENT_DATA_URI, mCallDataId));
builder.withValues(mCallDataValues);
operationList.add(builder.build());
}
mCallDataValues.clear(); // 通话数据处理完成,清空容器
mCallDataValues.clear();
}
// 5. 执行批量更新操作(如果有更新指令)
if (operationList.size() > 0) {
try {
// 批量执行数据库操作
ContentProviderResult[] results = context.getContentResolver().applyBatch(
Notes.AUTHORITY, operationList);
// 判断操作结果是否有效,返回便签的数据库地址
return (results == null || results.length == 0 || results[0] == null) ? null
: ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
} catch (RemoteException e) {
// 跨进程访问失败打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
} catch (OperationApplicationException e) {
// 批量操作执行失败打印日志并返回null
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
return null;
}
}
// 没有批量更新指令返回null表示无需更新或更新完成
return null;
}
}
}
}

@ -16,455 +16,402 @@
package net.micode.notes.model;
import android.appwidget.AppWidgetManager; // 桌面小组件管理类用于判断小组件ID是否有效
import android.content.ContentUris; // URI拼接工具拼接便签的数据库访问地址
import android.content.Context; // 上下文:获取数据库操作工具和资源
import android.database.Cursor; // 数据库查询结果游标:存储数据库查询返回的结果集
import android.text.TextUtils; // 文本工具类:判断字符串是否为空/空白
import android.util.Log; // 日志工具:打印错误/调试信息
import net.micode.notes.data.Notes; // 便签常量类存储便签类型、文件夹ID等固定值
import net.micode.notes.data.Notes.CallNote; // 通话便签常量:存储通话便签的字段名
import net.micode.notes.data.Notes.DataColumns; // 便签数据表字段:数据表的列名
import net.micode.notes.data.Notes.DataConstants; // 便签数据类型常量:区分文本/通话便签
import net.micode.notes.data.Notes.NoteColumns; // 便签主表字段:主表的列名
import net.micode.notes.data.Notes.TextNote; // 文本便签常量:存储文本便签的字段名
import net.micode.notes.tool.ResourceParser.NoteBgResources; // 便签背景资源工具根据颜色ID获取背景资源
/**
* 便
* 便便
* 便便便便
*/
import android.appwidget.AppWidgetManager;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.tool.ResourceParser.NoteBgResources;
public class WorkingNote {
// 关联的Note数据对象负责和本地数据库交互存储/同步便签数据)
// Note for the working note
private Note mNote;
// 便签唯一ID对应数据库里的便签ID0表示新便签未存入数据库
// Note Id
private long mNoteId;
// 便签内容:文本便签的正文文字
// Note content
private String mContent;
// 便签模式区分普通文本模式和清单模式比如0=普通模式1=清单模式)
// Note title
private String mTitle;
// Note mode
private int mMode;
// 提醒时间戳便签的提醒日期0表示无提醒
private long mAlertDate;
// 修改时间戳:便签最后一次修改的时间
private long mModifiedDate;
// 背景颜色ID便签的背景色标识对应不同的背景样式
private int mBgColorId;
// 桌面小组件ID关联的桌面便签小组件ID无效时为INVALID_APPWIDGET_ID
private int mWidgetId;
// 桌面小组件类型关联的桌面小组件类型无效时为TYPE_WIDGET_INVALIDE
private int mWidgetType;
// 所属文件夹ID便签所在的文件夹ID比如通话记录文件夹、自定义文件夹
private long mFolderId;
// 上下文:用于获取数据库工具、资源等
private Context mContext;
// 日志标签:打印该类的日志,方便排查问题
private static final String TAG = "WorkingNote";
// 删除标记是否标记为已删除true=已删除,无需保存)
private boolean mIsDeleted;
// 便签设置变化监听器:用于通知界面便签设置的改变(比如背景色、提醒时间变化)
private NoteSettingChangedListener mNoteSettingStatusListener;
// 便签数据表查询投影:查询便签数据时,要获取的字段列表(相当于查询结果的列清单)
public static final String[] DATA_PROJECTION = new String[] {
DataColumns.ID, // 0: 数据记录ID
DataColumns.CONTENT, // 1: 数据内容(文本便签的正文)
DataColumns.MIME_TYPE, // 2: 数据类型(文本/通话便签)
DataColumns.DATA1, // 3: 扩展字段1存储便签模式
DataColumns.DATA2, // 4: 扩展字段2
DataColumns.DATA3, // 5: 扩展字段3
DataColumns.DATA4, // 6: 扩展字段4
DataColumns.ID,
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
DataColumns.DATA1,
DataColumns.DATA2,
DataColumns.DATA3,
DataColumns.DATA4,
};
// 便签主表查询投影:查询便签主信息时,要获取的字段列表
public static final String[] NOTE_PROJECTION = new String[] {
NoteColumns.PARENT_ID, // 0: 所属文件夹ID
NoteColumns.ALERTED_DATE,// 1: 提醒时间
NoteColumns.BG_COLOR_ID, // 2: 背景颜色ID
NoteColumns.WIDGET_ID, // 3: 桌面小组件ID
NoteColumns.WIDGET_TYPE, // 4: 桌面小组件类型
NoteColumns.MODIFIED_DATE// 5: 修改时间
NoteColumns.PARENT_ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE,
NoteColumns.TITLE
};
// 数据表查询结果的列索引对应DATA_PROJECTION的字段位置方便快速取值
private static final int DATA_ID_COLUMN = 0; // 数据记录ID的索引
private static final int DATA_CONTENT_COLUMN = 1; // 数据内容的索引
private static final int DATA_MIME_TYPE_COLUMN = 2; // 数据类型的索引
private static final int DATA_MODE_COLUMN = 3; // 便签模式的索引
// 主表查询结果的列索引对应NOTE_PROJECTION的字段位置方便快速取值
private static final int NOTE_PARENT_ID_COLUMN = 0; // 所属文件夹ID的索引
private static final int NOTE_ALERTED_DATE_COLUMN = 1; // 提醒时间的索引
private static final int NOTE_BG_COLOR_ID_COLUMN = 2; // 背景颜色ID的索引
private static final int NOTE_WIDGET_ID_COLUMN = 3; // 桌面小组件ID的索引
private static final int NOTE_WIDGET_TYPE_COLUMN = 4; // 桌面小组件类型的索引
private static final int NOTE_MODIFIED_DATE_COLUMN = 5; // 修改时间的索引
// 私有构造方法:创建新便签(未存入数据库)
// 通俗说:初始化一个空白便签,设置默认属性
private static final int DATA_ID_COLUMN = 0;
private static final int DATA_CONTENT_COLUMN = 1;
private static final int DATA_MIME_TYPE_COLUMN = 2;
private static final int DATA_MODE_COLUMN = 3;
private static final int NOTE_PARENT_ID_COLUMN = 0;
private static final int NOTE_ALERTED_DATE_COLUMN = 1;
private static final int NOTE_BG_COLOR_ID_COLUMN = 2;
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;
// New note construct
private WorkingNote(Context context, long folderId) {
mContext = context;
mAlertDate = 0; // 默认无提醒
mModifiedDate = System.currentTimeMillis(); // 默认修改时间为当前时间
mFolderId = folderId; // 设置所属文件夹ID
mNote = new Note(); // 创建关联的Note数据对象
mNoteId = 0; // 新便签ID为0未存入数据库
mIsDeleted = false; // 默认未删除
mMode = 0; // 默认普通文本模式
mWidgetType = Notes.TYPE_WIDGET_INVALIDE; // 默认无有效桌面小组件
}
// 私有构造方法:加载已有便签(从数据库读取)
// 通俗说根据便签ID从数据库加载已有便签的所有信息
mAlertDate = 0;
mModifiedDate = System.currentTimeMillis();
mFolderId = folderId;
mNote = new Note();
mNoteId = 0;
mIsDeleted = false;
mMode = 0;
mWidgetType = Notes.TYPE_WIDGET_INVALIDE;
}
// Existing note construct
private WorkingNote(Context context, long noteId, long folderId) {
mContext = context;
mNoteId = noteId; // 设置已有便签的ID
mFolderId = folderId; // 设置所属文件夹ID
mIsDeleted = false; // 默认未删除
mNote = new Note(); // 创建关联的Note数据对象
loadNote(); // 加载便签主信息
mNoteId = noteId;
mFolderId = folderId;
mIsDeleted = false;
mNote = new Note();
loadNote();
}
/**
* 便
* 便ID便ID
*/
private void loadNote() {
// 拼接该便签的数据库访问地址,查询主表信息
// 执行查询不添加用户过滤条件因为NotesProvider已经处理了公开便签的访问权限
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId),
NOTE_PROJECTION, // 要查询的字段列表
null, null, null);
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
if (cursor != null) {
// 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
// 从查询结果中读取各字段值,赋值给成员变量
mFolderId = cursor.getLong(NOTE_PARENT_ID_COLUMN);
mBgColorId = cursor.getInt(NOTE_BG_COLOR_ID_COLUMN);
mWidgetId = cursor.getInt(NOTE_WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
mTitle = cursor.getString(NOTE_TITLE_COLUMN);
}
cursor.close(); // 关闭游标,释放资源
cursor.close();
} else {
// 未查询到便签,打印错误日志并抛出异常
Log.e(TAG, "No note with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note with id " + mNoteId);
}
loadNoteData(); // 主信息加载完成后,加载便签具体数据
loadNoteData();
}
/**
* 便
* 便ID便便
*/
private void loadNoteData() {
// 查询该便签对应的所有数据记录条件NOTE_ID等于当前便签ID
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", // 查询条件
new String[] { String.valueOf(mNoteId) }, // 条件参数
null);
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
}, null);
// 初始化默认值
mContent = "";
mMode = 0;
if (cursor != null) {
// 如果查询到结果,移动到第一条记录
if (cursor.moveToFirst()) {
// 循环遍历所有数据记录(一个便签可能对应多条数据,比如文本+通话记录)
do {
String type = cursor.getString(DATA_MIME_TYPE_COLUMN); // 获取数据类型
String type = cursor.getString(DATA_MIME_TYPE_COLUMN);
if (DataConstants.NOTE.equals(type)) {
// 文本便签读取内容、模式并绑定数据ID
mContent = cursor.getString(DATA_CONTENT_COLUMN);
mMode = cursor.getInt(DATA_MODE_COLUMN);
mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN));
} else if (DataConstants.CALL_NOTE.equals(type)) {
// 通话便签绑定通话数据ID
mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN));
} else {
// 未知数据类型,打印调试日志
Log.d(TAG, "Wrong note type with type:" + type);
}
} while (cursor.moveToNext()); // 移动到下一条记录,继续遍历
} while (cursor.moveToNext());
}
cursor.close(); // 关闭游标,释放资源
cursor.close();
} else {
// 未查询到便签数据,打印错误日志并抛出异常
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
/**
* 便
* 便
* @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); // 创建空白便签
note.setBgColorId(defaultBgColorId); // 设置默认背景色
note.setWidgetId(widgetId); // 设置桌面小组件ID
note.setWidgetType(widgetType); // 设置桌面小组件类型
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
note.setWidgetType(widgetType);
return note;
}
/**
* 便
* 便ID便
* @param context
* @param id 便ID
* @return WorkingNote
*/
public static WorkingNote load(Context context, long id) {
return new WorkingNote(context, id, 0);
}
/**
* 便
* 便/
* @return true=false=/
*/
public synchronized boolean saveNote() {
// 先判断是否值得保存(已删除、空白新便签、无修改的旧便签都无需保存)
if (isWorthSaving()) {
// 如果是新便签未存入数据库先生成新便签ID
if (!existInDatabase()) {
mNoteId = Note.getNewNoteId(mContext, mFolderId);
if (mNoteId == 0) {
// 生成新ID失败打印错误日志并返回失败
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
try {
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
Log.e(TAG, "Create new note fail with id:" + mNoteId);
return false;
}
}
}
// 调用Note对象的同步方法将修改写入数据库
mNote.syncNote(mContext, mNoteId);
mNote.syncNote(mContext, mNoteId);
/**
* 便
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
// 自动分类逻辑
try {
// 优先使用标题分类,如果标题为空则使用内容分类
String contentForCategory = mTitle;
if (contentForCategory == null || contentForCategory.isEmpty()) {
contentForCategory = mContent;
}
// 根据标题或内容自动分类
String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
// 创建或获取对应的文件夹
long categoryFolderId = net.micode.notes.tool.DataUtils.createFolder(mContext.getContentResolver(), category);
if (categoryFolderId > 0 && mFolderId != categoryFolderId) {
// 将便签移动到分类文件夹
net.micode.notes.tool.DataUtils.moveNoteToFoler(mContext.getContentResolver(), mNoteId, mFolderId, categoryFolderId);
mFolderId = categoryFolderId;
}
} catch (Exception e) {
Log.e(TAG, "Auto categorize fail: " + e.getMessage());
// 自动分类失败不影响便签保存
}
/**
* Update widget content if there exist any widget of this note
*/
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE
&& mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
return true;
} else {
return false;
}
return true; // 保存成功
} else {
return false; // 无需保存
} catch (Exception e) {
Log.e(TAG, "Save note fail: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* 便
* 便ID0便
* @return true=false=便
*/
public boolean existInDatabase() {
return mNoteId > 0;
}
/**
* 便
*
* 1. 2. 便3. 便
* @return true=false=
*/
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
if (mIsDeleted) {
return false;
} else {
// 允许保存空便签
return true;
}
}
/**
* 便
* 便便便
* @param l
*/
public void setOnSettingStatusChangedListener(NoteSettingChangedListener l) {
mNoteSettingStatusListener = l;
}
/**
* 便
* 便
* @param date
* @param set true=false=
*/
public void setAlertDate(long date, boolean set) {
if (date != mAlertDate) {
mAlertDate = date; // 更新提醒时间
// 将提醒时间存入Note对象准备同步到数据库
mAlertDate = date;
mNote.setNoteValue(NoteColumns.ALERTED_DATE, String.valueOf(mAlertDate));
}
// 如果设置了监听器,通知提醒时间变化
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onClockAlertChanged(date, set);
}
}
/**
* 便/
* 便
* @param mark true=false=
*/
public void markDeleted(boolean mark) {
mIsDeleted = mark; // 更新删除标记
// 如果关联了有效桌面小组件且设置了监听器,通知小组件变化
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
}
}
/**
* 便ID
* 便
* @param id ID
*/
public void setBgColorId(int id) {
if (id != mBgColorId) {
mBgColorId = id; // 更新背景颜色ID
// 如果设置了监听器,通知背景色变化
mBgColorId = id;
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged();
}
// 将背景颜色ID存入Note对象准备同步到数据库
mNote.setNoteValue(NoteColumns.BG_COLOR_ID, String.valueOf(id));
}
}
/**
* 便/
* 便
* @param mode 便0=1=
*/
public void setCheckListMode(int mode) {
if (mMode != mode) {
// 如果设置了监听器,通知模式变化(传入旧模式和新模式)
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onCheckListModeChanged(mMode, mode);
}
mMode = mode; // 更新便签模式
// 将便签模式存入Note对象准备同步到数据库
mMode = mode;
mNote.setTextData(TextNote.MODE, String.valueOf(mMode));
}
}
/**
*
* 便
* @param type
*/
public void setWidgetType(int type) {
if (type != mWidgetType) {
mWidgetType = type; // 更新小组件类型
// 将小组件类型存入Note对象准备同步到数据库
mWidgetType = type;
mNote.setNoteValue(NoteColumns.WIDGET_TYPE, String.valueOf(mWidgetType));
}
}
/**
* ID
* 便ID
* @param id ID
*/
public void setWidgetId(int id) {
if (id != mWidgetId) {
mWidgetId = id; // 更新小组件ID
// 将小组件ID存入Note对象准备同步到数据库
mWidgetId = id;
mNote.setNoteValue(NoteColumns.WIDGET_ID, String.valueOf(mWidgetId));
}
}
/**
* 便
* 便
* @param text 便
*/
public void setWorkingText(String text) {
if (!TextUtils.equals(mContent, text)) {
mContent = text; // 更新便签内容
// 将新内容存入Note对象准备同步到数据库
mContent = text;
mNote.setTextData(DataColumns.CONTENT, mContent);
}
}
public void setWorkingTitle(String title) {
if (!TextUtils.equals(mTitle, title)) {
mTitle = title;
mNote.setNoteValue(NoteColumns.TITLE, title);
}
}
public String getTitle() {
return mTitle;
}
/**
* 便
* 便便
* @param phoneNumber
* @param callDate
*/
public void convertToCallNote(String phoneNumber, long callDate) {
// 存入通话时间和号码
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));
mNote.setCallData(CallNote.PHONE_NUMBER, phoneNumber);
// 将便签移动到通话记录文件夹
mNote.setNoteValue(NoteColumns.PARENT_ID, String.valueOf(Notes.ID_CALL_RECORD_FOLDER));
}
/**
*
* 0便
* @return true=false=
*/
public boolean hasClockAlert() {
return (mAlertDate > 0 ? true : false);
}
// 以下都是属性获取方法:返回对应的便签属性值,供外部界面使用
public String getContent() { return mContent; }
public long getAlertDate() { return mAlertDate; }
public long getModifiedDate() { return mModifiedDate; }
public int getBgColorResId() { return NoteBgResources.getNoteBgResource(mBgColorId); }
public int getBgColorId() { return mBgColorId; }
public int getTitleBgResId() { return NoteBgResources.getNoteTitleBgResource(mBgColorId); }
public int getCheckListMode() { return mMode; }
public long getNoteId() { return mNoteId; }
public long getFolderId() { return mFolderId; }
public int getWidgetId() { return mWidgetId; }
public int getWidgetType() { return mWidgetType; }
/**
* 便
* 便UI
*/
public String getContent() {
return mContent;
}
public long getAlertDate() {
return mAlertDate;
}
public long getModifiedDate() {
return mModifiedDate;
}
public int getBgColorResId() {
return NoteBgResources.getNoteBgResource(mBgColorId);
}
public int getBgColorId() {
return mBgColorId;
}
public int getTitleBgResId() {
return NoteBgResources.getNoteTitleBgResource(mBgColorId);
}
public int getCheckListMode() {
return mMode;
}
public long getNoteId() {
return mNoteId;
}
public long getFolderId() {
return mFolderId;
}
public int getWidgetId() {
return mWidgetId;
}
public int getWidgetType() {
return mWidgetType;
}
public interface NoteSettingChangedListener {
/**
* 便
* 便
* Called when the background color of current note has just changed
*/
void onBackgroundColorChanged();
/**
* 便
* /便UI
* 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);
}
}
}

@ -0,0 +1,90 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.text.TextUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 便
*/
public class CategoryUtil {
// 分类关键词映射表
private static final Map<String, String> CATEGORY_KEYWORDS = new HashMap<>();
static {
// 工作相关
addKeywords("工作", "工作", "任务", "项目", "会议", "报告", "加班", "上班", "下班", "同事", "领导", "客户", "公司");
// 学习相关
addKeywords("学习", "学习", "考试", "作业", "复习", "预习", "课程", "老师", "学生", "学校", "教材", "笔记", "知识点");
// 生活相关
addKeywords("生活", "生活", "日常", "购物", "吃饭", "旅游", "电影", "音乐", "健身", "运动", "休息", "睡觉", "起床");
// 想法相关
addKeywords("想法", "想法", "创意", "灵感", "思考", "感悟", "心得", "体会", "观点", "意见", "建议");
// 待办相关
addKeywords("待办", "待办", "todo", "需要", "必须", "应该", "计划", "安排", "准备");
// 其他默认分类
addKeywords("其他", "");
}
// 批量添加关键词
private static void addKeywords(String category, String... keywords) {
for (String keyword : keywords) {
CATEGORY_KEYWORDS.put(keyword, category);
}
}
/**
* 便
* @param content 便
* @return 3
*/
public static String autoCategorize(String content) {
if (TextUtils.isEmpty(content)) {
return "其他";
}
// 转为小写进行匹配
String lowerContent = content.toLowerCase();
// 遍历关键词,匹配分类
for (Map.Entry<String, String> entry : CATEGORY_KEYWORDS.entrySet()) {
String keyword = entry.getKey();
String category = entry.getValue();
// 跳过空关键词(默认分类)
if (TextUtils.isEmpty(keyword)) {
continue;
}
// 关键词匹配
if (lowerContent.contains(keyword.toLowerCase())) {
return category;
}
}
// 默认分类
return "其他";
}
}

@ -81,7 +81,7 @@ public class DataUtils {
}
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
long folderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
@ -91,6 +91,21 @@ public class DataUtils {
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
// 如果是移动到回收站保存原始父文件夹ID
if (folderId == Notes.ID_TRASH_FOLER) {
// 查询当前父文件夹ID
Cursor cursor = resolver.query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
new String[]{NoteColumns.PARENT_ID},
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
long originParentId = cursor.getLong(0);
builder.withValue(NoteColumns.ORIGIN_PARENT_ID, originParentId);
cursor.close();
}
}
builder.withValue(NoteColumns.PARENT_ID, folderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
operationList.add(builder.build());
@ -184,8 +199,8 @@ public class DataUtils {
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
@ -247,7 +262,7 @@ public class DataUtils {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL("
+ CallNote.PHONE_NUMBER + ",?)",
+ CallNote.PHONE_NUMBER + ",?)",
new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber },
null);
@ -279,7 +294,8 @@ public class DataUtils {
cursor.close();
return snippet;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
// 如果找不到noteId返回空字符串而不是抛出异常
return "";
}
public static String getFormattedSnippet(String snippet) {
@ -292,4 +308,61 @@ public class DataUtils {
}
return snippet;
}
/**
* ID
* @param resolver ContentResolver
* @param folderName
* @return ID0
*/
public static long getFolderIdByName(ContentResolver resolver, String folderName) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.ID },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "=? AND " + NoteColumns.SNIPPET + "=?",
new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_ROOT_FOLDER), folderName },
null);
long folderId = 0;
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
folderId = cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get folder id failed: " + e.toString());
}
}
cursor.close();
}
return folderId;
}
/**
*
* @param resolver ContentResolver
* @param folderName
* @return ID0
*/
public static long createFolder(ContentResolver resolver, String folderName) {
// 检查文件夹是否已存在
long existingFolderId = getFolderIdByName(resolver, folderName);
if (existingFolderId > 0) {
return existingFolderId;
}
// 创建新文件夹
ContentValues values = new ContentValues();
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.SNIPPET, folderName);
values.put(NoteColumns.NOTES_COUNT, 0);
android.net.Uri uri = resolver.insert(Notes.CONTENT_NOTE_URI, values);
if (uri != null) {
return ContentUris.parseId(uri);
}
Log.e(TAG, "Create folder failed: " + folderName);
return 0;
}
}

@ -0,0 +1,192 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.data.Users;
/**
*
*/
public class UserManager {
private static final String TAG = "UserManager";
private static final String PREF_NAME = "user_preferences";
private static final String KEY_CURRENT_USER_ID = "current_user_id";
private static final String KEY_CURRENT_USERNAME = "current_username";
private static UserManager sInstance;
private SharedPreferences mPrefs;
private Context mContext;
private UserManager(Context context) {
mContext = context.getApplicationContext();
mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public static synchronized UserManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new UserManager(context);
}
return sInstance;
}
/**
*
* @param userId ID
* @param username
*/
public void saveCurrentUser(long userId, String username) {
SharedPreferences.Editor editor = mPrefs.edit();
editor.putLong(KEY_CURRENT_USER_ID, userId);
editor.putString(KEY_CURRENT_USERNAME, username);
editor.apply();
}
/**
* ID
* @return ID-1
*/
public long getCurrentUserId() {
return mPrefs.getLong(KEY_CURRENT_USER_ID, -1);
}
/**
*
* @return null
*/
public String getCurrentUsername() {
return mPrefs.getString(KEY_CURRENT_USERNAME, null);
}
/**
* 退
*/
public void clearCurrentUser() {
SharedPreferences.Editor editor = mPrefs.edit();
editor.remove(KEY_CURRENT_USER_ID);
editor.remove(KEY_CURRENT_USERNAME);
editor.apply();
}
/**
*
* @return
*/
public boolean isLoggedIn() {
return getCurrentUserId() != -1;
}
/**
*
* @param userId ID
* @param password
* @return
*/
public boolean validatePassword(long userId, String password) {
try {
// 直接访问数据库验证密码
NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = null;
try {
String selection = Users.UserColumns.ID + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
String[] selectionArgs = {String.valueOf(userId), password};
cursor = db.query(
NotesDatabaseHelper.TABLE.USER,
new String[]{Users.UserColumns.ID},
selection,
selectionArgs,
null,
null,
null
);
return cursor != null && cursor.moveToFirst();
} finally {
if (cursor != null) {
cursor.close();
}
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
*
* @param userId ID
*/
public void setCurrentUser(long userId) {
try {
// 查询用户的用户名
NotesDatabaseHelper helper = NotesDatabaseHelper.getInstance(mContext);
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = null;
String username = "未知用户";
try {
String selection = Users.UserColumns.ID + " = ?";
String[] selectionArgs = {String.valueOf(userId)};
cursor = db.query(
NotesDatabaseHelper.TABLE.USER,
new String[]{Users.UserColumns.USERNAME},
selection,
selectionArgs,
null,
null,
null
);
if (cursor != null && cursor.moveToFirst()) {
username = cursor.getString(0);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// 保存当前用户信息
SharedPreferences.Editor editor = mPrefs.edit();
editor.putLong(KEY_CURRENT_USER_ID, userId);
editor.putString(KEY_CURRENT_USERNAME, username);
editor.apply();
} catch (Exception e) {
Log.e(TAG, "Error in setCurrentUser: " + e.getMessage());
e.printStackTrace();
// 即使发生异常也确保保存用户ID避免状态不一致
try {
SharedPreferences.Editor editor = mPrefs.edit();
editor.putLong(KEY_CURRENT_USER_ID, userId);
editor.putString(KEY_CURRENT_USERNAME, "未知用户");
editor.apply();
} catch (Exception innerE) {
Log.e(TAG, "Error in emergency save: " + innerE.getMessage());
innerE.printStackTrace();
}
}
}
}

@ -30,6 +30,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
@ -52,30 +53,42 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Window win = getWindow();
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// 添加适当的标志,确保在各种情况下都能显示提醒窗口
win.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
// 在Android 10+中需要确保Intent有正确的标志
// 注意FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TASK是Intent的常量不是WindowManager.LayoutParams的常量
// 这些标志已经在AlarmReceiver中通过intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)设置
if (!isScreenOn()) {
win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
}
Intent intent = getIntent();
try {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} catch (IllegalArgumentException e) {
e.printStackTrace();
// 检查Intent和数据是否存在
if (intent != null && intent.getData() != null) {
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
mSnippet = mSnippet != null && mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
: mSnippet;
} else {
Log.e("AlarmAlertActivity", "Intent or data is null");
finish();
return;
}
} catch (Exception e) {
Log.e("AlarmAlertActivity", "Error processing intent: " + e.getMessage(), e);
finish();
return;
}
mPlayer = new MediaPlayer();
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
if (mNoteId > 0 && DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
playAlarmSound();
} else {
@ -85,44 +98,72 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
private boolean isScreenOn() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
// 在Android 10+中isScreenOn()方法已被弃用应使用isInteractive()
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) {
return pm.isInteractive();
} else {
return pm.isScreenOn();
}
}
private void playAlarmSound() {
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
// 添加空检查,避免崩溃
if (mPlayer == null) {
return;
}
try {
Uri url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM);
if (url == null) {
// 如果没有默认的闹钟铃声,使用系统默认铃声
url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
}
if (url == null) {
// 如果没有系统默认铃声,使用通知铃声
url = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_NOTIFICATION);
}
if (url == null) {
// 如果都没有,直接返回
return;
}
int silentModeStreams = Settings.System.getInt(getContentResolver(),
Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0);
if ((silentModeStreams & (1 << AudioManager.STREAM_ALARM)) != 0) {
mPlayer.setAudioStreamType(silentModeStreams);
} else {
mPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
mPlayer.setDataSource(this, url);
mPlayer.prepare();
mPlayer.setLooping(true);
mPlayer.start();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
// 发生异常时,释放播放器资源
if (mPlayer != null) {
try {
mPlayer.release();
mPlayer = null;
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
private void showActionDialog() {
AlertDialog.Builder dialog = new AlertDialog.Builder(this);
dialog.setTitle(R.string.app_name);
dialog.setMessage(mSnippet);
// 如果mSnippet为空显示默认提示信息
if (mSnippet == null || mSnippet.isEmpty()) {
dialog.setMessage(getString(R.string.set_remind_time_message));
} else {
dialog.setMessage(mSnippet);
}
dialog.setPositiveButton(R.string.notealert_ok, this);
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
@ -150,9 +191,15 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
try {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
mPlayer = null;
}
}
}
}

@ -53,10 +53,17 @@ public class AlarmInitReceiver extends BroadcastReceiver {
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
Intent sender = new Intent(context, AlarmReceiver.class);
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 使用适当的PendingIntent flag确保在Android 12+中正常工作
int flags = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ? PendingIntent.FLAG_IMMUTABLE : 0;
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, flags);
AlarmManager alermManager = (AlarmManager) context
.getSystemService(Context.ALARM_SERVICE);
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
// 在Android 6.0+中使用setExact方法以确保准确的提醒时间
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
alermManager.setExact(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} else {
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
}
} while (c.moveToNext());
}
c.close();

@ -23,8 +23,17 @@ import android.content.Intent;
public class AlarmReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
intent.setClass(context, AlarmAlertActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
// 添加空检查,避免崩溃
if (context == null || intent == null) {
return;
}
try {
intent.setClass(context, AlarmAlertActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
}
}
}

@ -0,0 +1,432 @@
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.text.TextUtils;
import android.graphics.Color;
import net.micode.notes.R;
import net.micode.notes.data.Messages;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.tool.UserManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class ChatActivity extends Activity {
private static final String TAG = "ChatActivity";
private ListView mChatListView;
private EditText mMessageEditText;
private Button mSendButton;
private ChatAdapter mChatAdapter;
private List<ChatMessage> mMessageList;
private NotesDatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
private UserManager mUserManager;
private long mCurrentUserId;
private long mFriendId;
private String mFriendUsername;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
setContentView(R.layout.chat_activity);
// 设置ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
// 获取Intent参数
mFriendId = getIntent().getLongExtra("friend_id", -1);
mFriendUsername = getIntent().getStringExtra("friend_username");
if (mFriendId == -1 || mFriendUsername == null) {
Toast.makeText(this, "无效的好友信息", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 设置ActionBar标题为好友用户名
if (actionBar != null) {
actionBar.setTitle(mFriendUsername);
}
// 初始化数据库
mDbHelper = NotesDatabaseHelper.getInstance(this);
if (mDbHelper != null) {
mDb = mDbHelper.getWritableDatabase();
}
// 初始化UserManager
mUserManager = UserManager.getInstance(this);
if (mUserManager != null) {
mCurrentUserId = mUserManager.getCurrentUserId();
}
// 初始化界面控件
mChatListView = findViewById(R.id.chat_list);
mMessageEditText = findViewById(R.id.message_edit_text);
mSendButton = findViewById(R.id.send_button);
// 初始化消息列表
mMessageList = new ArrayList<>();
mChatAdapter = new ChatAdapter(this, mMessageList, mCurrentUserId);
mChatListView.setAdapter(mChatAdapter);
// 设置发送按钮点击事件
mSendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendMessage();
}
});
// 加载聊天记录
loadChatHistory();
} catch (Exception e) {
Log.e(TAG, "Error in onCreate: " + e.getMessage(), e);
Toast.makeText(this, "聊天界面初始化失败", Toast.LENGTH_SHORT).show();
finish();
}
}
@Override
protected void onResume() {
super.onResume();
try {
// 重新获取当前用户ID确保在账号切换后能使用正确的用户ID
if (mUserManager != null) {
mCurrentUserId = mUserManager.getCurrentUserId();
Log.d(TAG, "Updated current user ID to: " + mCurrentUserId);
}
// 更新适配器的当前用户ID
if (mChatAdapter != null) {
mChatAdapter.mCurrentUserId = mCurrentUserId;
}
// 重新加载聊天记录
loadChatHistory();
} catch (Exception e) {
Log.e(TAG, "Error in onResume: " + e.getMessage(), e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭数据库连接
if (mDb != null && mDb.isOpen()) {
mDb.close();
}
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 返回上一级活动
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
*
*/
private void loadChatHistory() {
mMessageList.clear();
// 查询聊天记录
String sql = "SELECT * FROM " + NotesDatabaseHelper.TABLE.MESSAGE + " WHERE " +
"(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) OR " +
"(" + Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ?) " +
"ORDER BY " + Messages.MessageColumns.CREATED_DATE + " ASC";
Cursor cursor = mDb.rawQuery(sql, new String[]{
String.valueOf(mCurrentUserId), String.valueOf(mFriendId),
String.valueOf(mFriendId), String.valueOf(mCurrentUserId)
});
if (cursor != null) {
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.ID));
long senderId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.SENDER_ID));
long receiverId = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.RECEIVER_ID));
String content = cursor.getString(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CONTENT));
int messageType = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.MESSAGE_TYPE));
long createdDate = cursor.getLong(cursor.getColumnIndexOrThrow(Messages.MessageColumns.CREATED_DATE));
int isRead = cursor.getInt(cursor.getColumnIndexOrThrow(Messages.MessageColumns.IS_READ));
mMessageList.add(new ChatMessage(id, senderId, receiverId, content, messageType, createdDate, isRead));
}
cursor.close();
}
// 通知适配器数据变化
mChatAdapter.notifyDataSetChanged();
// 滚动到底部
if (!mMessageList.isEmpty()) {
mChatListView.setSelection(mMessageList.size() - 1);
}
// 将接收到的消息标记为已读
markMessagesAsRead();
}
/**
*
*/
private void sendMessage() {
String content = mMessageEditText.getText().toString().trim();
if (content.isEmpty()) {
Toast.makeText(this, "消息内容不能为空", Toast.LENGTH_SHORT).show();
return;
}
// 创建消息对象
ContentValues values = new ContentValues();
values.put(Messages.MessageColumns.SENDER_ID, mCurrentUserId);
values.put(Messages.MessageColumns.RECEIVER_ID, mFriendId);
values.put(Messages.MessageColumns.CONTENT, content);
values.put(Messages.MessageColumns.MESSAGE_TYPE, Messages.MessageType.TEXT);
values.put(Messages.MessageColumns.CREATED_DATE, System.currentTimeMillis());
values.put(Messages.MessageColumns.IS_READ, 0);
// 插入消息到数据库
long messageId = mDb.insert(NotesDatabaseHelper.TABLE.MESSAGE, null, values);
if (messageId != -1) {
// 清空输入框
mMessageEditText.setText("");
// 重新加载聊天记录
loadChatHistory();
} else {
Toast.makeText(this, "发送失败", Toast.LENGTH_SHORT).show();
Log.e(TAG, "Failed to send message");
}
}
/**
*
*/
private void markMessagesAsRead() {
ContentValues values = new ContentValues();
values.put(Messages.MessageColumns.IS_READ, 1);
int updatedRows = mDb.update(NotesDatabaseHelper.TABLE.MESSAGE, values,
Messages.MessageColumns.SENDER_ID + " = ? AND " + Messages.MessageColumns.RECEIVER_ID + " = ? AND " + Messages.MessageColumns.IS_READ + " = 0",
new String[]{String.valueOf(mFriendId), String.valueOf(mCurrentUserId)});
Log.d(TAG, "Marked " + updatedRows + " messages as read");
}
/**
* 便
*/
private void showNoteDetail(String noteData) {
try {
// 解析便签数据
String[] noteParts = noteData.split("\\|");
if (noteParts.length < 2) {
Toast.makeText(this, "无效的便签数据", Toast.LENGTH_SHORT).show();
return;
}
String noteTitle = noteParts[0];
String noteContent = noteParts[1];
if (TextUtils.isEmpty(noteTitle)) {
noteTitle = "无标题便签";
}
// 创建并显示便签详情对话框
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(noteTitle);
builder.setMessage(noteContent);
builder.setPositiveButton("确定", null);
builder.show();
} catch (Exception e) {
Log.e(TAG, "Error showing note detail: " + e.getMessage(), e);
Toast.makeText(this, "显示便签详情失败", Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
private static class ChatMessage {
long id;
long senderId;
long receiverId;
String content;
int messageType;
long createdDate;
int isRead;
ChatMessage(long id, long senderId, long receiverId, String content, int messageType, long createdDate, int isRead) {
this.id = id;
this.senderId = senderId;
this.receiverId = receiverId;
this.content = content;
this.messageType = messageType;
this.createdDate = createdDate;
this.isRead = isRead;
}
}
/**
*
*/
private static class ChatAdapter extends BaseAdapter {
private static final int VIEW_TYPE_SENT_TEXT = 0;
private static final int VIEW_TYPE_RECEIVED_TEXT = 1;
private static final int VIEW_TYPE_SENT_NOTE = 2;
private static final int VIEW_TYPE_RECEIVED_NOTE = 3;
private Context mContext;
private List<ChatMessage> mMessageList;
public long mCurrentUserId;
private LayoutInflater mInflater;
private SimpleDateFormat mDateFormat;
ChatAdapter(Context context, List<ChatMessage> messageList, long currentUserId) {
mContext = context;
mMessageList = messageList;
mCurrentUserId = currentUserId;
mInflater = LayoutInflater.from(context);
mDateFormat = new SimpleDateFormat("HH:mm");
}
@Override
public int getCount() {
return mMessageList.size();
}
@Override
public Object getItem(int position) {
return mMessageList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getViewTypeCount() {
return 4; // 四种视图类型:发送文本、接收文本、发送便签、接收便签
}
@Override
public int getItemViewType(int position) {
ChatMessage message = mMessageList.get(position);
boolean isSentByMe = message.senderId == mCurrentUserId;
if (message.messageType == Messages.MessageType.NOTE) {
return isSentByMe ? VIEW_TYPE_SENT_NOTE : VIEW_TYPE_RECEIVED_NOTE;
} else {
return isSentByMe ? VIEW_TYPE_SENT_TEXT : VIEW_TYPE_RECEIVED_TEXT;
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ChatMessage message = mMessageList.get(position);
boolean isSentByMe = message.senderId == mCurrentUserId;
ViewHolder holder;
if (convertView == null) {
// 根据视图类型选择不同的布局
int viewType = getItemViewType(position);
switch (viewType) {
case VIEW_TYPE_SENT_NOTE:
convertView = mInflater.inflate(R.layout.chat_message_sent_note_item, parent, false);
break;
case VIEW_TYPE_RECEIVED_NOTE:
convertView = mInflater.inflate(R.layout.chat_message_received_note_item, parent, false);
break;
case VIEW_TYPE_SENT_TEXT:
default:
convertView = mInflater.inflate(R.layout.chat_message_sent_item, parent, false);
break;
case VIEW_TYPE_RECEIVED_TEXT:
convertView = mInflater.inflate(R.layout.chat_message_received_item, parent, false);
break;
}
holder = new ViewHolder();
holder.contentTextView = convertView.findViewById(R.id.message_content);
holder.timeTextView = convertView.findViewById(R.id.message_time);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
// 设置消息内容
if (message.messageType == Messages.MessageType.NOTE) {
// 便签类型消息,只显示便签标题
String[] noteData = message.content.split("\\|");
if (noteData.length >= 2) {
String noteTitle = noteData[0];
if (TextUtils.isEmpty(noteTitle)) {
noteTitle = "无标题便签";
}
holder.contentTextView.setText(noteTitle);
// 设置不同的颜色,区分于普通文本消息
holder.contentTextView.setTextColor(Color.BLUE); // 使用蓝色区分便签消息
holder.contentTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击便签,查看详情
((ChatActivity) mContext).showNoteDetail(message.content);
}
});
}
} else {
// 普通文本消息
holder.contentTextView.setText(message.content);
holder.contentTextView.setOnClickListener(null);
}
// 设置消息时间
Date date = new Date(message.createdDate);
holder.timeTextView.setText(mDateFormat.format(date));
return convertView;
}
private static class ViewHolder {
TextView contentTextView;
TextView timeTextView;
}
}
}

@ -0,0 +1,206 @@
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.tool.UserManager;
import net.micode.notes.data.Users;
import java.util.ArrayList;
import java.util.List;
public class FriendManagementActivity extends Activity {
private ListView mFriendListView;
private FriendAdapter mFriendAdapter;
private List<Friend> mFriendList;
private NotesDatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
private UserManager mUserManager;
private long mCurrentUserId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_friend_management);
// 设置ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle("好友管理");
}
// 初始化数据库
mDbHelper = NotesDatabaseHelper.getInstance(this);
mDb = mDbHelper.getWritableDatabase();
// 初始化UserManager
mUserManager = UserManager.getInstance(this);
mCurrentUserId = mUserManager.getCurrentUserId();
// 初始化ListView
mFriendListView = findViewById(R.id.friend_list);
mFriendList = new ArrayList<>();
mFriendAdapter = new FriendAdapter(this, mFriendList);
mFriendListView.setAdapter(mFriendAdapter);
// 设置ListView点击事件
mFriendListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Friend friend = mFriendList.get(position);
// 启动与好友聊天的活动
Intent intent = new Intent(FriendManagementActivity.this, ChatActivity.class);
intent.putExtra("friend_id", friend.id);
intent.putExtra("friend_username", friend.username);
startActivity(intent);
}
});
// 加载好友列表
loadFriendList();
}
@Override
protected void onResume() {
super.onResume();
// 重新加载好友列表
loadFriendList();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭数据库连接
if (mDb != null && mDb.isOpen()) {
mDb.close();
}
}
/**
*
*/
private void loadFriendList() {
mFriendList.clear();
// 查询除当前用户以外的所有用户
Cursor cursor = mDb.query(
NotesDatabaseHelper.TABLE.USER,
new String[]{Users.UserColumns.ID, Users.UserColumns.USERNAME},
Users.UserColumns.ID + " != ?",
new String[]{String.valueOf(mCurrentUserId)},
null,
null,
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
String username = cursor.getString(cursor.getColumnIndexOrThrow(Users.UserColumns.USERNAME));
mFriendList.add(new Friend(id, username));
}
cursor.close();
}
// 通知适配器数据变化
mFriendAdapter.notifyDataSetChanged();
// 如果没有好友,显示提示
if (mFriendList.isEmpty()) {
Toast.makeText(this, "暂无其他用户", Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
private static class Friend {
long id;
String username;
Friend(long id, String username) {
this.id = id;
this.username = username;
}
}
/**
*
*/
private static class FriendAdapter extends BaseAdapter {
private Context mContext;
private List<Friend> mFriendList;
private LayoutInflater mInflater;
FriendAdapter(Context context, List<Friend> friendList) {
mContext = context;
mFriendList = friendList;
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mFriendList.size();
}
@Override
public Object getItem(int position) {
return mFriendList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_friend, parent, false);
holder = new ViewHolder();
holder.usernameTextView = convertView.findViewById(R.id.friend_username);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Friend friend = mFriendList.get(position);
holder.usernameTextView.setText(friend.username);
return convertView;
}
private static class ViewHolder {
TextView usernameTextView;
}
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 返回上一级活动
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

@ -0,0 +1,239 @@
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
public class FriendNoteEditActivity extends Activity {
private TextView mNoteEditor;
private TextView mNoteTitleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_friend_note_edit);
// 设置ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle("查看便签");
}
// 初始化控件
mNoteEditor = findViewById(R.id.note_edit_view);
mNoteTitleView = findViewById(R.id.note_title_view);
// 加载便签数据
if (savedInstanceState == null && !initActivityState(getIntent())) {
finish();
return;
}
initNoteScreen();
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(Intent.EXTRA_UID)) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, savedInstanceState.getLong(Intent.EXTRA_UID));
intent.putExtra("friend_id", savedInstanceState.getLong("friend_id"));
if (!initActivityState(intent)) {
finish();
return;
}
// 恢复状态后重新初始化界面
initNoteScreen();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (getIntent() != null) {
outState.putLong(Intent.EXTRA_UID, getIntent().getLongExtra(Intent.EXTRA_UID, 0));
outState.putLong("friend_id", getIntent().getLongExtra("friend_id", -1));
}
}
private String mNoteContent;
private String mNoteTitle;
private long mModifiedDate;
private boolean initActivityState(Intent intent) {
/**
*
*/
long noteId = intent.getLongExtra(Intent.EXTRA_UID, 0);
long friendId = intent.getLongExtra("friend_id", -1);
if (noteId <= 0 || friendId <= 0) {
Toast.makeText(this, "便签信息错误", Toast.LENGTH_SHORT).show();
finish();
return false;
}
try {
// 查询便签基本信息,包括标题
String[] noteProjection = {
net.micode.notes.data.Notes.NoteColumns.MODIFIED_DATE,
net.micode.notes.data.Notes.NoteColumns.TITLE
};
android.database.Cursor noteCursor = null;
try {
// 使用ContentResolver查询便签基本信息
noteCursor = getContentResolver().query(
android.content.ContentUris.withAppendedId(net.micode.notes.data.Notes.CONTENT_NOTE_URI, noteId),
noteProjection,
null,
null,
null
);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
mModifiedDate = noteCursor.getLong(0);
mNoteTitle = noteCursor.getString(1);
}
noteCursor.close();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "查询便签信息失败", Toast.LENGTH_SHORT).show();
}
// 查询便签具体内容这是必须的因为完整内容存储在Data表中
String[] dataProjection = {net.micode.notes.data.Notes.DataColumns.CONTENT};
String dataSelection = net.micode.notes.data.Notes.DataColumns.NOTE_ID + " = ? AND " +
net.micode.notes.data.Notes.DataColumns.MIME_TYPE + " = ?";
String[] dataSelectionArgs = {String.valueOf(noteId), net.micode.notes.data.Notes.DataConstants.NOTE};
android.database.Cursor dataCursor = null;
try {
// 使用ContentResolver查询数据
dataCursor = getContentResolver().query(
net.micode.notes.data.Notes.CONTENT_DATA_URI,
dataProjection,
dataSelection,
dataSelectionArgs,
null
);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
mNoteContent = dataCursor.getString(0);
}
dataCursor.close();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "查询便签内容失败", Toast.LENGTH_SHORT).show();
} finally {
if (dataCursor != null) {
dataCursor.close();
}
}
// 如果查询失败,尝试使用数据库直接查询
if (mNoteContent == null || mNoteContent.isEmpty()) {
try {
// 直接使用数据库查询绕过ContentProvider的用户过滤
net.micode.notes.data.NotesDatabaseHelper helper = net.micode.notes.data.NotesDatabaseHelper.getInstance(FriendNoteEditActivity.this);
if (helper != null) {
dataCursor = helper.getReadableDatabase().query(
net.micode.notes.data.NotesDatabaseHelper.TABLE.DATA,
dataProjection,
dataSelection,
dataSelectionArgs,
null,
null,
null
);
if (dataCursor != null) {
if (dataCursor.moveToFirst()) {
mNoteContent = dataCursor.getString(0);
}
dataCursor.close();
}
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "直接查询便签内容失败", Toast.LENGTH_SHORT).show();
} finally {
if (dataCursor != null) {
dataCursor.close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "初始化便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
finish();
return false;
}
return true;
}
private void initNoteScreen() {
// 显示修改日期
TextView modifiedDateTextView = findViewById(R.id.tv_modified_date);
modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
this, mModifiedDate,
android.text.format.DateUtils.FORMAT_SHOW_DATE |
android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
android.text.format.DateUtils.FORMAT_SHOW_TIME));
// 显示便签标题
if (mNoteTitle != null && !mNoteTitle.isEmpty()) {
mNoteTitleView.setVisibility(View.VISIBLE);
mNoteTitleView.setText(mNoteTitle);
} else {
// 如果没有标题,隐藏标题文本视图
mNoteTitleView.setVisibility(View.GONE);
}
// 显示便签内容,添加调试日志
if (mNoteContent != null && !mNoteContent.isEmpty()) {
mNoteEditor.setText(mNoteContent);
// 设置文本颜色为深色,确保可见
mNoteEditor.setTextColor(getResources().getColor(android.R.color.primary_text_dark));
// 设置背景色为白色,确保可见
mNoteEditor.setBackgroundColor(getResources().getColor(android.R.color.white));
} else {
// 如果内容为空,显示提示文本
mNoteEditor.setText("(空便签)");
mNoteEditor.setTextColor(getResources().getColor(android.R.color.secondary_text_dark));
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 不显示菜单,因为是只读模式
return false;
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 返回上一级活动
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

@ -0,0 +1,271 @@
package net.micode.notes.ui;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.WorkingNote;
import java.util.ArrayList;
import java.util.List;
public class FriendNoteListActivity extends Activity {
private ListView mNoteListView;
private NoteAdapter mNoteAdapter;
private List<Note> mNoteList;
private long mFriendId;
private String mFriendUsername;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_friend_note_list);
// 获取好友信息
mFriendId = getIntent().getLongExtra("friend_id", -1);
mFriendUsername = getIntent().getStringExtra("friend_username");
if (mFriendId == -1 || mFriendUsername == null) {
Toast.makeText(this, "好友信息错误", Toast.LENGTH_SHORT).show();
finish();
return;
}
// 设置ActionBar
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(mFriendUsername + "的公开便签");
}
// 初始化ListView
mNoteListView = findViewById(R.id.friend_note_list);
mNoteList = new ArrayList<>();
mNoteAdapter = new NoteAdapter(this, mNoteList);
mNoteListView.setAdapter(mNoteAdapter);
// 设置ListView点击事件
mNoteListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Note note = mNoteList.get(position);
// 启动查看便签详情的活动
Intent intent = new Intent(FriendNoteListActivity.this, FriendNoteEditActivity.class);
intent.putExtra(Intent.EXTRA_UID, note.id);
intent.putExtra("friend_id", mFriendId);
startActivity(intent);
}
});
// 加载好友的公开便签
loadFriendNotes();
}
@Override
protected void onResume() {
super.onResume();
// 重新加载好友便签
loadFriendNotes();
}
/**
* 便
*/
private void loadFriendNotes() {
try {
mNoteList.clear();
// 查询好友的所有公开便签
String[] projection = {
NoteColumns.ID,
NoteColumns.TITLE,
NoteColumns.SNIPPET,
NoteColumns.MODIFIED_DATE,
NoteColumns.TYPE,
NoteColumns.PINNED,
NoteColumns.LOCKED,
NoteColumns.PUBLIC
};
String selection = NoteColumns.USER_ID + " = ? AND " + NoteColumns.PUBLIC + " = 1 AND " + NoteColumns.PARENT_ID + " <> " + Notes.ID_TRASH_FOLER;
String[] selectionArgs = {String.valueOf(mFriendId)};
Cursor cursor = null;
try {
cursor = getContentResolver().query(
Notes.CONTENT_NOTE_URI,
projection,
selection,
selectionArgs,
NoteColumns.PINNED + " DESC, " + NoteColumns.MODIFIED_DATE + " DESC"
);
if (cursor != null) {
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
String title = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.TITLE));
String content = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
int type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
int pinned = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PINNED));
int locked = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCKED));
int isPublic = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.PUBLIC));
mNoteList.add(new Note(id, title, content, modifiedDate, type, pinned, locked, isPublic));
}
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "查询好友便签失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
} finally {
if (cursor != null) {
cursor.close();
}
}
// 通知适配器数据变化
mNoteAdapter.notifyDataSetChanged();
// 如果没有公开便签,显示提示
if (mNoteList.isEmpty()) {
Toast.makeText(this, mFriendUsername + "没有公开的便签", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "加载好友便签失败", Toast.LENGTH_SHORT).show();
}
}
/**
* 便
*/
private static class Note {
long id;
String title;
String content;
long modifiedDate;
int type;
int pinned;
int locked;
int isPublic;
Note(long id, String title, String content, long modifiedDate, int type, int pinned, int locked, int isPublic) {
this.id = id;
this.title = title;
this.content = content;
this.modifiedDate = modifiedDate;
this.type = type;
this.pinned = pinned;
this.locked = locked;
this.isPublic = isPublic;
}
}
/**
* 便
*/
private static class NoteAdapter extends BaseAdapter {
private Activity mActivity;
private List<Note> mNoteList;
private LayoutInflater mInflater;
NoteAdapter(Activity activity, List<Note> noteList) {
mActivity = activity;
mNoteList = noteList;
mInflater = LayoutInflater.from(activity);
}
@Override
public int getCount() {
return mNoteList.size();
}
@Override
public Object getItem(int position) {
return mNoteList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.note_item, parent, false);
holder = new ViewHolder();
holder.titleTextView = convertView.findViewById(R.id.tv_title);
holder.modifiedDateTextView = convertView.findViewById(R.id.tv_time);
holder.pinnedImageView = convertView.findViewById(R.id.iv_alert_icon);
holder.lockedImageView = convertView.findViewById(R.id.iv_lock_icon);
holder.publicImageView = convertView.findViewById(R.id.iv_public_icon);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Note note = mNoteList.get(position);
// 显示标题,如果标题为空则显示内容摘要
String displayText;
if (note.title != null && !note.title.isEmpty()) {
displayText = note.title;
} else {
displayText = note.content;
}
holder.titleTextView.setText(displayText);
holder.modifiedDateTextView.setText(android.text.format.DateUtils.formatDateTime(
mActivity, note.modifiedDate,
android.text.format.DateUtils.FORMAT_SHOW_DATE |
android.text.format.DateUtils.FORMAT_NUMERIC_DATE |
android.text.format.DateUtils.FORMAT_SHOW_TIME));
// 设置置顶图标
holder.pinnedImageView.setVisibility(note.pinned == 1 ? View.VISIBLE : View.GONE);
// 设置锁定图标
holder.lockedImageView.setVisibility(note.locked == 1 ? View.VISIBLE : View.GONE);
// 设置公开图标
holder.publicImageView.setVisibility(note.isPublic == 1 ? View.VISIBLE : View.GONE);
return convertView;
}
private static class ViewHolder {
TextView titleTextView;
TextView modifiedDateTextView;
ImageView pinnedImageView;
ImageView lockedImageView;
ImageView publicImageView;
}
}
@Override
public boolean onOptionsItemSelected(android.view.MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 返回上一级活动
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

@ -0,0 +1,321 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.data.Users;
import net.micode.notes.tool.UserManager;
public class LoginActivity extends AppCompatActivity {
private EditText etUsername;
private EditText etPassword;
private NotesDatabaseHelper mDbHelper;
private SQLiteDatabase mDb;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 初始化数据库
mDbHelper = NotesDatabaseHelper.getInstance(this);
mDb = mDbHelper.getWritableDatabase();
// 初始化控件
etUsername = findViewById(R.id.et_username);
etPassword = findViewById(R.id.et_password);
}
/**
*
*/
public void onLoginClick(View view) {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
// 验证输入
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "用户名和密码不能为空", Toast.LENGTH_SHORT).show();
return;
}
// 验证用户名和密码
long userId = validateUser(username, password);
if (userId != -1) {
// 登录成功,保存用户信息
UserManager userManager = UserManager.getInstance(this);
userManager.saveCurrentUser(userId, username);
// 跳转到便签列表
Intent intent = new Intent(this, NotesListActivity.class);
startActivity(intent);
finish();
} else {
// 登录失败,提示错误
Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show();
}
}
/**
*
*/
public void onRegisterClick(View view) {
showRegisterDialog();
}
/**
*
*/
public void onChangePasswordClick(View view) {
showChangePasswordDialog();
}
/**
*
*/
private void showRegisterDialog() {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_register, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(dialogView);
final AlertDialog dialog = builder.create();
// 初始化注册对话框控件
final EditText etRegisterUsername = dialogView.findViewById(R.id.et_register_username);
final EditText etRegisterPassword = dialogView.findViewById(R.id.et_register_password);
final EditText etRegisterConfirmPassword = dialogView.findViewById(R.id.et_register_confirm_password);
Button btnRegisterCancel = dialogView.findViewById(R.id.btn_register_cancel);
Button btnRegisterConfirm = dialogView.findViewById(R.id.btn_register_confirm);
// 取消按钮点击事件
btnRegisterCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
// 注册按钮点击事件
btnRegisterConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = etRegisterUsername.getText().toString().trim();
String password = etRegisterPassword.getText().toString().trim();
String confirmPassword = etRegisterConfirmPassword.getText().toString().trim();
// 验证输入
if (username.isEmpty() || password.isEmpty() || confirmPassword.isEmpty()) {
Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
return;
}
if (!password.equals(confirmPassword)) {
Toast.makeText(LoginActivity.this, "两次输入的密码不一致", Toast.LENGTH_SHORT).show();
return;
}
// 检查用户名是否已存在
if (isUsernameExists(username)) {
Toast.makeText(LoginActivity.this, "用户名已存在", Toast.LENGTH_SHORT).show();
return;
}
// 注册用户
if (registerUser(username, password)) {
Toast.makeText(LoginActivity.this, "注册成功", Toast.LENGTH_SHORT).show();
dialog.dismiss();
} else {
Toast.makeText(LoginActivity.this, "注册失败,请重试", Toast.LENGTH_SHORT).show();
}
}
});
dialog.show();
}
/**
*
*/
private void showChangePasswordDialog() {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(dialogView);
final AlertDialog dialog = builder.create();
// 初始化修改密码对话框控件
final EditText etChangeUsername = dialogView.findViewById(R.id.et_change_username);
final EditText etCurrentPassword = dialogView.findViewById(R.id.et_current_password);
final EditText etNewPassword = dialogView.findViewById(R.id.et_new_password);
final EditText etConfirmNewPassword = dialogView.findViewById(R.id.et_confirm_new_password);
Button btnChangeCancel = dialogView.findViewById(R.id.btn_change_cancel);
Button btnChangeConfirm = dialogView.findViewById(R.id.btn_change_confirm);
// 取消按钮点击事件
btnChangeCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialog.dismiss();
}
});
// 确认修改按钮点击事件
btnChangeConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = etChangeUsername.getText().toString().trim();
String currentPassword = etCurrentPassword.getText().toString().trim();
String newPassword = etNewPassword.getText().toString().trim();
String confirmNewPassword = etConfirmNewPassword.getText().toString().trim();
// 验证输入
if (username.isEmpty() || currentPassword.isEmpty() || newPassword.isEmpty() || confirmNewPassword.isEmpty()) {
Toast.makeText(LoginActivity.this, "请填写完整信息", Toast.LENGTH_SHORT).show();
return;
}
if (!newPassword.equals(confirmNewPassword)) {
Toast.makeText(LoginActivity.this, "两次输入的新密码不一致", Toast.LENGTH_SHORT).show();
return;
}
// 验证当前密码
if (validateUser(username, currentPassword) == -1) {
Toast.makeText(LoginActivity.this, "用户名或当前密码错误", Toast.LENGTH_SHORT).show();
return;
}
// 修改密码
if (changePassword(username, newPassword)) {
Toast.makeText(LoginActivity.this, "密码修改成功", Toast.LENGTH_SHORT).show();
dialog.dismiss();
} else {
Toast.makeText(LoginActivity.this, "密码修改失败,请重试", Toast.LENGTH_SHORT).show();
}
}
});
dialog.show();
}
/**
* ID
*/
private long validateUser(String username, String password) {
String[] projection = {Users.UserColumns.ID};
String selection = Users.UserColumns.USERNAME + " = ? AND " + Users.UserColumns.PASSWORD + " = ?";
String[] selectionArgs = {username, password};
Cursor cursor = mDb.query(
NotesDatabaseHelper.TABLE.USER,
projection,
selection,
selectionArgs,
null,
null,
null
);
long userId = -1;
if (cursor.moveToFirst()) {
userId = cursor.getLong(cursor.getColumnIndexOrThrow(Users.UserColumns.ID));
}
cursor.close();
return userId;
}
/**
*
*/
private boolean isUsernameExists(String username) {
String[] projection = {Users.UserColumns.ID};
String selection = Users.UserColumns.USERNAME + " = ?";
String[] selectionArgs = {username};
Cursor cursor = mDb.query(
NotesDatabaseHelper.TABLE.USER,
projection,
selection,
selectionArgs,
null,
null,
null
);
boolean exists = cursor.getCount() > 0;
cursor.close();
return exists;
}
/**
*
*/
private boolean registerUser(String username, String password) {
ContentValues values = new ContentValues();
values.put(Users.UserColumns.USERNAME, username);
values.put(Users.UserColumns.PASSWORD, password);
long result = mDb.insert(NotesDatabaseHelper.TABLE.USER, null, values);
return result != -1;
}
/**
*
*/
private boolean changePassword(String username, String newPassword) {
ContentValues values = new ContentValues();
values.put(Users.UserColumns.PASSWORD, newPassword);
values.put(Users.UserColumns.MODIFIED_DATE, System.currentTimeMillis());
String selection = Users.UserColumns.USERNAME + " = ?";
String[] selectionArgs = {username};
int result = mDb.update(
NotesDatabaseHelper.TABLE.USER,
values,
selection,
selectionArgs
);
return result > 0;
}
@Override
protected void onDestroy() {
super.onDestroy();
// 关闭数据库连接
if (mDb != null && mDb.isOpen()) {
mDb.close();
}
}
}

@ -17,12 +17,26 @@
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
@ -30,6 +44,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import net.micode.notes.R;
@ -101,24 +116,32 @@ public class NoteEditText extends EditText {
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
x += getScrollX();
y += getScrollY();
Layout layout = getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
Selection.setSelection(getText(), off);
break;
// 确保获得焦点,无论是否有内容
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (!hasFocus()) {
requestFocus();
}
}
return super.onTouchEvent(event);
// 调用父类方法处理事件
boolean handled = super.onTouchEvent(event);
// 处理点击事件
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// 无论是否有layout都确保光标在正确位置
setSelection(getText().length());
// 显示软键盘
Context context = getContext();
if (context != null) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT);
}
}
}
return handled;
}
@Override
@ -214,4 +237,201 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
/**
*
*/
public void setBold() {
toggleStyle(Typeface.BOLD);
}
/**
*
*/
public void setItalic() {
toggleStyle(Typeface.ITALIC);
}
/**
*
*/
public void setBoldItalic() {
applyStyle(Typeface.BOLD_ITALIC);
}
/**
*
*/
public void setNormal() {
applyStyle(Typeface.NORMAL);
}
/**
* 线
*/
public void toggleUnderline() {
toggleSpan(UnderlineSpan.class);
}
/**
* 线
*/
public void toggleStrikethrough() {
toggleSpan(StrikethroughSpan.class);
}
/**
*
*/
public void setTextColor(int color) {
applySpan(new ForegroundColorSpan(color));
}
/**
*
*/
public void setTextBackgroundColor(int color) {
applySpan(new BackgroundColorSpan(color));
}
/**
*
*/
public void setTextSize(float size) {
// 将字号稍微放大一些
float adjustedSize = size * 1.1f;
applySpan(new RelativeSizeSpan(adjustedSize));
}
/**
*
*/
public void setAlignLeft() {
setGravity(android.view.Gravity.LEFT);
}
/**
*
*/
public void setAlignCenter() {
setGravity(android.view.Gravity.CENTER);
}
/**
*
*/
public void setAlignRight() {
setGravity(android.view.Gravity.RIGHT);
}
/**
*
*/
public void setAlignJustify() {
setGravity(android.view.Gravity.FILL_HORIZONTAL);
}
/**
*
*/
private void applyStyle(int style) {
Spannable spannable = getText();
if (spannable == null) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end) return;
StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
for (StyleSpan span : spans) {
spannable.removeSpan(span);
}
spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setSelection(start, end);
}
/**
*
*/
private void toggleStyle(int style) {
Spannable spannable = getText();
if (spannable == null) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end) return;
// 检查当前样式
boolean hasStyle = false;
StyleSpan[] spans = spannable.getSpans(start, end, StyleSpan.class);
for (StyleSpan span : spans) {
if (span.getStyle() == style) {
hasStyle = true;
spannable.removeSpan(span);
}
}
if (!hasStyle) {
// 没有该样式,添加
spannable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
setSelection(start, end);
}
/**
* Span
*/
private void applySpan(Object span) {
Spannable spannable = getText();
if (spannable == null) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end) return;
spannable.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
setSelection(start, end);
}
/**
* Span
*/
private void toggleSpan(Class<?> spanClass) {
Spannable spannable = getText();
if (spannable == null) return;
int start = getSelectionStart();
int end = getSelectionEnd();
if (start == end) return;
// 检查当前是否有该Span
boolean hasSpan = false;
try {
Object[] spans = spannable.getSpans(start, end, spanClass);
for (Object span : spans) {
hasSpan = true;
spannable.removeSpan(span);
}
if (!hasSpan) {
// 没有该Span添加
try {
// 创建该Span的实例
Object newSpan = spanClass.getDeclaredConstructor().newInstance();
spannable.setSpan(newSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
}
}
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
}
setSelection(start, end);
}
}

@ -37,12 +37,17 @@ public class NoteItemData {
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TITLE,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.PINNED,
NoteColumns.SORT_ORDER,
NoteColumns.LOCKED,
NoteColumns.PUBLIC,
};
private static final int ID_COLUMN = 0;
public static final int ID_COLUMN = 0;
private static final int ALERTED_DATE_COLUMN = 1;
private static final int BG_COLOR_ID_COLUMN = 2;
private static final int CREATED_DATE_COLUMN = 3;
@ -51,9 +56,14 @@ public class NoteItemData {
private static final int NOTES_COUNT_COLUMN = 6;
private static final int PARENT_ID_COLUMN = 7;
private static final int SNIPPET_COLUMN = 8;
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
private static final int TITLE_COLUMN = 9;
private static final int TYPE_COLUMN = 10;
private static final int WIDGET_ID_COLUMN = 11;
private static final int WIDGET_TYPE_COLUMN = 12;
private static final int PINNED_COLUMN = 13;
private static final int SORT_ORDER_COLUMN = 14;
private static final int LOCKED_COLUMN = 15;
private static final int PUBLIC_COLUMN = 16;
private long mId;
private long mAlertDate;
@ -64,9 +74,14 @@ public class NoteItemData {
private int mNotesCount;
private long mParentId;
private String mSnippet;
private String mTitle;
private int mType;
private int mWidgetId;
private int mWidgetType;
private boolean mPinned;
private int mSortOrder;
private boolean mLocked;
private boolean mPublic;
private String mName;
private String mPhoneNumber;
@ -88,9 +103,14 @@ public class NoteItemData {
mSnippet = cursor.getString(SNIPPET_COLUMN);
mSnippet = mSnippet.replace(NoteEditActivity.TAG_CHECKED, "").replace(
NoteEditActivity.TAG_UNCHECKED, "");
mTitle = cursor.getString(TITLE_COLUMN);
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
mPinned = (cursor.getInt(PINNED_COLUMN) > 0) ? true : false;
mSortOrder = cursor.getInt(SORT_ORDER_COLUMN);
mLocked = (cursor.getInt(LOCKED_COLUMN) > 0) ? true : false;
mPublic = (cursor.getInt(PUBLIC_COLUMN) > 0) ? true : false;
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
@ -218,6 +238,26 @@ public class NoteItemData {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
public boolean isPinned() {
return mPinned;
}
public int getSortOrder() {
return mSortOrder;
}
public boolean isLocked() {
return mLocked;
}
public boolean isPublic() {
return mPublic;
}
public String getTitle() {
return mTitle;
}
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}

@ -37,6 +37,7 @@ public class NotesListAdapter extends CursorAdapter {
private HashMap<Integer, Boolean> mSelectedIndex;
private int mNotesCount;
private boolean mChoiceMode;
private String mSearchQuery;
public static class AppWidgetAttribute {
public int widgetId;
@ -48,6 +49,13 @@ public class NotesListAdapter extends CursorAdapter {
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
mSearchQuery = null;
}
// 设置搜索查询
public void setSearchQuery(String query) {
mSearchQuery = query;
notifyDataSetChanged();
}
@Override
@ -60,7 +68,7 @@ public class NotesListAdapter extends CursorAdapter {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor);
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
isSelectedItem(cursor.getPosition()), mSearchQuery);
}
}
@ -91,17 +99,19 @@ public class NotesListAdapter extends CursorAdapter {
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id);
Cursor cursor = getCursor();
if (cursor != null) {
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
if (cursor.moveToPosition(position)) {
long id = cursor.getLong(NoteItemData.ID_COLUMN);
if (id != Notes.ID_ROOT_FOLDER) {
itemSet.add(id);
}
}
}
}
}
return itemSet;
}

@ -32,9 +32,12 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
public class NotesListItem extends LinearLayout {
private ImageView mAlert;
private ImageView mLock;
private ImageView mPublic;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
private TextView mCategory;
private NoteItemData mItemData;
private CheckBox mCheckBox;
@ -42,13 +45,16 @@ public class NotesListItem extends LinearLayout {
super(context);
inflate(context, R.layout.note_item, this);
mAlert = (ImageView) findViewById(R.id.iv_alert_icon);
mLock = (ImageView) findViewById(R.id.iv_lock_icon);
mPublic = (ImageView) findViewById(R.id.iv_public_icon);
mTitle = (TextView) findViewById(R.id.tv_title);
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCategory = (TextView) findViewById(R.id.tv_category);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked, String searchQuery) {
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@ -64,11 +70,20 @@ public class NotesListItem extends LinearLayout {
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
} else if (data.getId() == Notes.ID_TRASH_FOLER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText("回收站"
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
// 使用现有的clock图标作为临时垃圾桶图标
mAlert.setImageResource(R.drawable.clock);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
mTitle.setText(highlightText(formattedSnippet, searchQuery, context));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
@ -76,26 +91,69 @@ public class NotesListItem extends LinearLayout {
mAlert.setVisibility(View.GONE);
}
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
} else {
// 显示标题,如果标题为空则显示内容摘要
String displayText;
if (data.getTitle() != null && !data.getTitle().isEmpty()) {
displayText = data.getTitle();
} else {
String formattedSnippet = DataUtils.getFormattedSnippet(data.getSnippet());
displayText = formattedSnippet;
}
mTitle.setText(highlightText(displayText, searchQuery, context));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
}
}
}
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 设置分类标签,优先使用标题进行分类
String contentForCategory = data.getTitle();
if (contentForCategory == null || contentForCategory.isEmpty()) {
contentForCategory = data.getSnippet();
}
String category = net.micode.notes.tool.CategoryUtil.autoCategorize(contentForCategory);
mCategory.setText(category);
// 处理锁定图标
if (data.isLocked() && data.getType() == Notes.TYPE_NOTE) {
mLock.setVisibility(View.VISIBLE);
mLock.setImageResource(R.drawable.clock); // 使用clock图标作为锁图标
} else {
mLock.setVisibility(View.GONE);
}
// 处理公开图标
if (data.isPublic() && data.getType() == Notes.TYPE_NOTE) {
mPublic.setVisibility(View.VISIBLE);
mPublic.setImageResource(R.drawable.call_record); // 使用call_record图标作为公开图标与置顶图标区分
} else {
mPublic.setVisibility(View.GONE);
}
// 处理置顶和提醒图标
if (data.isPinned() && data.getType() == Notes.TYPE_NOTE) {
mAlert.setVisibility(View.VISIBLE);
mAlert.setImageResource(R.drawable.selected); // 使用selected图标作为置顶图标
} else if (data.hasAlert() && data.getType() == Notes.TYPE_NOTE) {
mAlert.setVisibility(View.VISIBLE);
mAlert.setImageResource(R.drawable.call_record); // 使用call_record图标作为提醒图标
} else {
mAlert.setVisibility(View.GONE);
}
setBackground(data);
}
@ -116,6 +174,33 @@ public class NotesListItem extends LinearLayout {
}
}
// 高亮匹配的文本
private CharSequence highlightText(String text, String searchQuery, Context context) {
if (text == null || searchQuery == null || searchQuery.isEmpty()) {
return text;
}
android.text.SpannableString spannable = new android.text.SpannableString(text);
try {
String lowerText = text.toLowerCase();
String lowerQuery = searchQuery.toLowerCase();
int startIndex = lowerText.indexOf(lowerQuery);
while (startIndex != -1) {
int endIndex = startIndex + searchQuery.length();
spannable.setSpan(
new android.text.style.BackgroundColorSpan(context.getResources().getColor(R.color.user_query_highlight)),
startIndex, endIndex, android.text.Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
startIndex = lowerText.indexOf(lowerQuery, endIndex);
}
} catch (Exception e) {
// 处理可能的异常,比如空指针或索引越界
return text;
}
return spannable;
}
public NoteItemData getItemData() {
return mItemData;
}

@ -39,6 +39,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
@ -57,7 +58,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
public static final String PREFERENCE_PASSWORD_KEY = "pref_key_password";
public static final String PREFERENCE_PASSWORD_SET_KEY = "pref_key_password_set";
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
private static final String PREFERENCE_PASSWORD_SETTING_KEY = "pref_password_setting_key";
private static final String AUTHORITIES_FILTER_KEY = "authorities";
@ -78,6 +84,50 @@ public class NotesPreferenceActivity extends PreferenceActivity {
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// Add password setting preference
Preference passwordPreference = new Preference(this);
passwordPreference.setTitle(getString(R.string.preferences_password_title));
passwordPreference.setSummary(getString(R.string.preferences_password_summary));
passwordPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
showPasswordSettingDialog();
return true;
}
});
// Add friend management preference
Preference friendPreference = new Preference(this);
friendPreference.setTitle("好友");
friendPreference.setSummary("管理和查看好友的公开便签");
friendPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 启动好友管理活动
Intent intent = new Intent(NotesPreferenceActivity.this, FriendManagementActivity.class);
startActivity(intent);
return true;
}
});
// Add change background preference
Preference changeBackgroundPreference = new Preference(this);
changeBackgroundPreference.setTitle("更换背景");
changeBackgroundPreference.setSummary("更换便签界面的背景图片");
changeBackgroundPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 直接启动NoteEditActivity并传递更换背景的标志
Intent intent = new Intent(NotesPreferenceActivity.this, NoteEditActivity.class);
intent.putExtra("CHANGE_BACKGROUND", true);
startActivity(intent);
return true;
}
});
PreferenceCategory generalCategory = (PreferenceCategory) getPreferenceScreen().getPreference(1);
generalCategory.addPreference(passwordPreference);
generalCategory.addPreference(friendPreference);
generalCategory.addPreference(changeBackgroundPreference);
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
@ -360,6 +410,93 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
public static boolean isPasswordSet(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getBoolean(PREFERENCE_PASSWORD_SET_KEY, false);
}
public static String getPassword(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_PASSWORD_KEY, "");
}
private void setPassword(String password) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putString(PREFERENCE_PASSWORD_KEY, password);
editor.putBoolean(PREFERENCE_PASSWORD_SET_KEY, !TextUtils.isEmpty(password));
editor.commit();
}
private void showPasswordSettingDialog() {
final boolean hasPassword = isPasswordSet(this);
View view = LayoutInflater.from(this).inflate(R.layout.password_setting_dialog, null);
final EditText currentPasswordEdit = (EditText) view.findViewById(R.id.current_password);
final EditText newPasswordEdit = (EditText) view.findViewById(R.id.new_password);
final EditText confirmPasswordEdit = (EditText) view.findViewById(R.id.confirm_password);
final TextView currentPasswordLabel = (TextView) view.findViewById(R.id.current_password_label);
if (!hasPassword) {
currentPasswordLabel.setVisibility(View.GONE);
currentPasswordEdit.setVisibility(View.GONE);
}
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
dialogBuilder.setTitle(hasPassword ? getString(R.string.preferences_password_change_title) : getString(R.string.preferences_password_set_title));
dialogBuilder.setView(view);
dialogBuilder.setPositiveButton(getString(R.string.preferences_button_confirm), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
String currentPassword = currentPasswordEdit.getText().toString();
String newPassword = newPasswordEdit.getText().toString();
String confirmPassword = confirmPasswordEdit.getText().toString();
if (hasPassword) {
// 验证当前密码
if (!currentPassword.equals(getPassword(NotesPreferenceActivity.this))) {
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_password_incorrect),
Toast.LENGTH_SHORT).show();
return;
}
}
// 验证新密码和确认密码
if (TextUtils.isEmpty(newPassword)) {
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_password_empty),
Toast.LENGTH_SHORT).show();
return;
}
if (!newPassword.equals(confirmPassword)) {
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_password_not_match),
Toast.LENGTH_SHORT).show();
return;
}
// 设置密码
setPassword(newPassword);
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_password_set_success),
Toast.LENGTH_SHORT).show();
}
});
dialogBuilder.setNegativeButton(getString(R.string.preferences_button_cancel), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
dialogBuilder.show();
}
private class GTaskReceiver extends BroadcastReceiver {
@Override

@ -0,0 +1,56 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import net.micode.notes.R;
public class SplashActivity extends Activity {
private static final int SPLASH_DISPLAY_DURATION = 2000; // 2秒
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// 找到小米logo容器
LinearLayout miLogo = findViewById(R.id.mi_logo);
// 创建淡入淡出动画
AlphaAnimation animation = new AlphaAnimation(0.0f, 1.0f);
animation.setDuration(1500); // 1.5秒
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// 动画开始时的回调
}
@Override
public void onAnimationEnd(Animation animation) {
// 动画结束后跳转到主界面
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent mainIntent = new Intent(SplashActivity.this, NotesListActivity.class);
SplashActivity.this.startActivity(mainIntent);
SplashActivity.this.finish();
}
}, 500); // 延迟500毫秒后跳转
}
@Override
public void onAnimationRepeat(Animation animation) {
// 动画重复时的回调
}
});
// 应用动画到小米logo
miLogo.startAnimation(animation);
}
}
Loading…
Cancel
Save