合并 #9

Closed
prwohun7f wants to merge 2 commits from zhaoxin_branch into master

@ -1,28 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.micode.notes"
android:versionCode="1"
android:versionName="0.1" >
<uses-sdk android:minSdkVersion="14" />
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
@ -31,10 +12,31 @@
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:icon="@drawable/icon_app"
android:label="@string/app_name" >
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Notesmaster"
tools:targetApi="31">
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.Notesmaster">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.LoginActivity"
android:exported="false"
android:theme="@style/Theme.Notesmaster">
</activity>
<activity
android:name=".ui.NotesListActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
@ -42,109 +44,122 @@
android:launchMode="singleTop"
android:theme="@style/NoteTheme"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustPan" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
android:windowSoftInputMode="adjustPan"
android:exported="false">
</activity>
<activity
android:name=".ui.NoteEditActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTop"
android:theme="@style/NoteTheme" >
<intent-filter>
android:theme="@style/NoteTheme"
android:exported="true">
<intent-filter >
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<intent-filter>
</intent-filter >>
<intent-filter >
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/text_note" />
<data android:mimeType="vnd.android.cursor.item/call_note" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<provider
android:name="net.micode.notes.data.NotesProvider"
android:authorities="micode_notes"
android:multiprocess="true" />
<receiver
android:name=".widget.NoteWidgetProvider_2x"
android:label="@string/app_widget2x2" >
android:label="@string/app_widget2x2"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_2x_info" />
</receiver>
<receiver
android:name=".widget.NoteWidgetProvider_4x"
android:label="@string/app_widget4x4" >
android:label="@string/app_widget4x4"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_DELETED" />
<action android:name="android.intent.action.PRIVACY_MODE_CHANGED" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_4x_info" />
</receiver>
<receiver android:name=".ui.AlarmInitReceiver" >
<receiver android:name=".ui.AlarmInitReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name="net.micode.notes.ui.AlarmReceiver"
android:process=":remote" >
</receiver>
<activity
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<activity
android:name="net.micode.notes.ui.FriendManagementActivity"
android:exported="false"
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<activity
android:name="net.micode.notes.ui.FriendNoteListActivity"
android:exported="false"
android:theme="@style/NoteTheme" >
</activity>
<activity
android:name="net.micode.notes.ui.FriendNoteEditActivity"
android:exported="false"
android:theme="@style/NoteTheme" >
</activity>
<activity
android:name="net.micode.notes.ui.ChatActivity"
android:exported="false"
android:theme="@style/NoteTheme"
android:configChanges="keyboardHidden|orientation|screenSize" >
</activity>
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >
</service>
<meta-data
android:name="android.app.default_searchable"
android:value=".ui.NoteEditActivity" />
<!-- <activity-->
<!-- android:name=".MainActivity"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />-->
<!-- </intent-filter>-->
<!-- </activity>-->
</application>
</manifest>
</manifest>

@ -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>

@ -0,0 +1,24 @@
package net.micode.notes;
import android.os.Bundle;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
}
}

@ -0,0 +1,30 @@
package net.micode.notes;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import net.micode.notes.ui.LoginActivity;
import net.micode.notes.ui.NotesListActivity;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
private static final long SPLASH_DURATION = 1500; // 1.5秒的启动动画
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
// 延迟后跳转到登录界面
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(SplashActivity.this, LoginActivity.class);
startActivity(intent);
finish();
}
}, SPLASH_DURATION);
}
}

@ -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;
}

@ -98,6 +98,12 @@ public class Notes {
* <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
@ -165,6 +171,36 @@ 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 {

@ -25,24 +25,30 @@ 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 {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 4;
private static final int DB_VERSION = 12;
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;
private static final String CREATE_NOTE_TABLE_SQL =
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," +
@ -53,6 +59,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
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," +
@ -60,7 +67,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.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 =
@ -82,6 +94,26 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
"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
*/
@ -287,17 +319,29 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
static synchronized NotesDatabaseHelper getInstance(Context context) {
public static synchronized NotesDatabaseHelper getInstance(Context context) {
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
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);
}
@Override
@ -322,6 +366,46 @@ 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);
@ -359,4 +443,34 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
}
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 ''");
}
}

@ -33,6 +33,7 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.NotesDatabaseHelper.TABLE;
import net.micode.notes.tool.UserManager;
public class NotesProvider extends ContentProvider {
@ -89,60 +90,193 @@ public class NotesProvider extends ContentProvider {
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
Cursor c = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
SQLiteDatabase db = null;
String id = null;
switch (mMatcher.match(uri)) {
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;
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");
}
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");
}
String searchString = null;
if (mMatcher.match(uri) == URI_SEARCH_SUGGEST) {
if (uri.getPathSegments().size() > 1) {
searchString = uri.getPathSegments().get(1);
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");
}
} else {
searchString = uri.getQueryParameter("pattern");
}
if (TextUtils.isEmpty(searchString)) {
return null;
}
if (TextUtils.isEmpty(searchString)) {
return null;
}
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);
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;
}
@ -151,8 +285,17 @@ public class NotesProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = mHelper.getWritableDatabase();
long dataId = 0, noteId = 0, insertedId = 0;
// 获取当前用户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:
@ -187,8 +330,24 @@ public class NotesProvider extends ContentProvider {
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)) {
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;
@ -202,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;
@ -233,13 +398,30 @@ public class NotesProvider extends ContentProvider {
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)) {
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);

@ -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 {
// 可以添加一些用户相关的常量
}
}

@ -63,25 +63,43 @@ public class GTaskASyncTask extends AsyncTask<Void, String, Integer> {
});
}
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) {
// 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);
}
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) {
publishProgess(mContext.getString(R.string.sync_progress_login, NotesPreferenceActivity

@ -30,6 +30,7 @@ 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;
@ -50,14 +51,22 @@ public class Note {
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);
long noteId = 0;
try {
noteId = Long.valueOf(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
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;
}
}
if (noteId == -1) {
throw new IllegalStateException("Wrong note id:" + noteId);

@ -39,6 +39,8 @@ public class WorkingNote {
private long mNoteId;
// Note content
private String mContent;
// Note title
private String mTitle;
// Note mode
private int mMode;
@ -78,7 +80,8 @@ public class WorkingNote {
NoteColumns.BG_COLOR_ID,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.MODIFIED_DATE
NoteColumns.MODIFIED_DATE,
NoteColumns.TITLE
};
private static final int DATA_ID_COLUMN = 0;
@ -101,6 +104,8 @@ public class WorkingNote {
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;
@ -125,6 +130,7 @@ public class WorkingNote {
}
private void loadNote() {
// 执行查询不添加用户过滤条件因为NotesProvider已经处理了公开便签的访问权限
Cursor cursor = mContext.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mNoteId), NOTE_PROJECTION, null,
null, null);
@ -137,6 +143,7 @@ public class WorkingNote {
mWidgetType = cursor.getInt(NOTE_WIDGET_TYPE_COLUMN);
mAlertDate = cursor.getLong(NOTE_ALERTED_DATE_COLUMN);
mModifiedDate = cursor.getLong(NOTE_MODIFIED_DATE_COLUMN);
mTitle = cursor.getString(NOTE_TITLE_COLUMN);
}
cursor.close();
} else {
@ -149,9 +156,13 @@ public class WorkingNote {
private void loadNoteData() {
Cursor cursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", new String[] {
String.valueOf(mNoteId)
String.valueOf(mNoteId)
}, null);
// 初始化默认值
mContent = "";
mMode = 0;
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
@ -170,12 +181,11 @@ public class WorkingNote {
cursor.close();
} else {
Log.e(TAG, "No data with id:" + mNoteId);
throw new IllegalArgumentException("Unable to find note's data with id " + mNoteId);
}
}
public static WorkingNote createEmptyNote(Context context, long folderId, int widgetId,
int widgetType, int defaultBgColorId) {
int widgetType, int defaultBgColorId) {
WorkingNote note = new WorkingNote(context, folderId);
note.setBgColorId(defaultBgColorId);
note.setWidgetId(widgetId);
@ -188,26 +198,54 @@ public class WorkingNote {
}
public synchronized boolean saveNote() {
if (isWorthSaving()) {
if (!existInDatabase()) {
if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) {
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;
}
}
}
mNote.syncNote(mContext, mNoteId);
mNote.syncNote(mContext, mNoteId);
// 自动分类逻辑
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();
/**
* 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 {
} catch (Exception e) {
Log.e(TAG, "Save note fail: " + e.getMessage());
e.printStackTrace();
return false;
}
}
@ -217,10 +255,10 @@ public class WorkingNote {
}
private boolean isWorthSaving() {
if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent))
|| (existInDatabase() && !mNote.isLocalModified())) {
if (mIsDeleted) {
return false;
} else {
// 允许保存空便签
return true;
}
}
@ -243,7 +281,7 @@ public class WorkingNote {
mIsDeleted = mark;
if (mWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID
&& mWidgetType != Notes.TYPE_WIDGET_INVALIDE && mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onWidgetChanged();
mNoteSettingStatusListener.onWidgetChanged();
}
}
@ -287,6 +325,17 @@ public class WorkingNote {
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;
}
public void convertToCallNote(String phoneNumber, long callDate) {
mNote.setCallData(CallNote.CALL_DATE, String.valueOf(callDate));

@ -5,7 +5,7 @@
* 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
* 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,
@ -14,61 +14,46 @@
* limitations under the License.
*/
// 包声明:归属小米便签的工具模块,提供笔记备份(导出为文本文件)核心功能
package net.micode.notes.tool;
// 导入安卓上下文类访问资源、ContentResolver
import android.content.Context;
// 导入安卓数据库游标类:查询便签/便签数据的核心载体
import android.database.Cursor;
// 导入安卓外部存储类判断SD卡挂载状态、获取SD卡根目录
import android.os.Environment;
// 导入安卓文本工具类:判空、字符串处理
import android.text.TextUtils;
// 导入安卓日期格式化类:格式化便签修改时间、导出文件名
import android.text.format.DateFormat;
// 导入安卓日志类:输出备份过程中的日志(调试/错误)
import android.util.Log;
// 导入小米便签资源类:引用字符串(文件路径、格式模板、文件夹名称)
import net.micode.notes.R;
// 导入便签数据常量类定义ContentURI、字段、便签类型、特殊文件夹ID等
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
// 导入文件操作相关类:创建文件/目录、文件输出流、打印流
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
/**
* 便
*
* 1.
* 2. 便便SD
* 3.
* - 便
* - 便
* - 便
* 4. 便/
* 5. SD
* 便便
* <p>
* 便便SD
* 便便
*/
public class BackupUtils {
// 日志标签:用于备份过程的日志输出
private static final String TAG = "BackupUtils";
// 单例实例全局唯一的BackupUtils对象
// Singleton stuff
private static BackupUtils sInstance;
/**
* 线
* 线
* @param context 访
* @return BackupUtils
* BackupUtils
* <p>
* BackupUtils
*
* @param context
* @return BackupUtils
*/
public static synchronized BackupUtils getInstance(Context context) {
if (sInstance == null) {
@ -78,76 +63,73 @@ public class BackupUtils {
}
/**
*
*
*/
// SD卡未挂载(无法创建/写入导出文件)
// 当前SD卡未挂载
public static final int STATE_SD_CARD_UNMOUONTED = 0;
// 备份文件不存在(仅恢复操作使用,当前导出逻辑未用到)
// 备份文件不存在
public static final int STATE_BACKUP_FILE_NOT_EXIST = 1;
// 数据格式错误(导出时未用到,预留恢复操作的状态码)
// 数据格式不正确,可能被其他程序修改
public static final int STATE_DATA_DESTROIED = 2;
// 系统错误如文件创建失败、IO异常等运行时错误
// 运行时异常导致备份或恢复失败
public static final int STATE_SYSTEM_ERROR = 3;
// 导出操作成功完成
// 备份或恢复成功
public static final int STATE_SUCCESS = 4;
// 文本导出器实例:封装实际的文本导出逻辑,与工具类解耦
private TextExport mTextExport;
/**
*
* @param context TextExport
*
*
* @param context
*/
private BackupUtils(Context context) {
mTextExport = new TextExport(context);
}
/**
* SD
* SD
* @return true=SDfalse=
*
*
* @return
*/
private static boolean externalStorageAvailable() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
/**
* 便
* @return BackupUtilsSTATE_XXX
* 便
* <p>
* TextExportexportToText
*
* @return STATE_*
*/
public int exportToText() {
return mTextExport.exportToText();
}
/**
*
* @return notes_20251223.txt
*
*
* @return
*/
public String getExportedTextFileName() {
return mTextExport.mFileName;
}
/**
*
* @return /sdcard/MiNotes/
*
*
* @return
*/
public String getExportedTextFileDir() {
return mTextExport.mFileDirectory;
}
/**
*
* BackupUtils便
*
*/
private static class TextExport {
/**
* 便IO
*
* - NoteColumns.ID便/ID
* - NoteColumns.MODIFIED_DATE便
* - NoteColumns.SNIPPET便/
* - NoteColumns.TYPE便/便/
*/
// 便签查询的投影列
private static final String[] NOTE_PROJECTION = {
NoteColumns.ID,
NoteColumns.MODIFIED_DATE,
@ -155,20 +137,11 @@ public class BackupUtils {
NoteColumns.TYPE
};
// 便签投影数组列索引常量简化Cursor取值避免硬编码
private static final int NOTE_COLUMN_ID = 0;
private static final int NOTE_COLUMN_MODIFIED_DATE = 1;
private static final int NOTE_COLUMN_SNIPPET = 2;
/**
* 便便/
*
* - DataColumns.CONTENT便/
* - DataColumns.MIME_TYPE便/
* - DataColumns.DATA1
* - DataColumns.DATA4
* DATA使
*/
// 便签数据查询的投影列
private static final String[] DATA_PROJECTION = {
DataColumns.CONTENT,
DataColumns.MIME_TYPE,
@ -178,198 +151,174 @@ public class BackupUtils {
DataColumns.DATA4,
};
// 便签数据投影数组列索引常量
private static final int DATA_COLUMN_CONTENT = 0;
private static final int DATA_COLUMN_MIME_TYPE = 1;
private static final int DATA_COLUMN_CALL_DATE = 2;
private static final int DATA_COLUMN_PHONE_NUMBER = 4;
/**
*
*
* - FORMAT_FOLDER_NAMEXXX
* - FORMAT_NOTE_DATE便2025-12-23 15:30
* - FORMAT_NOTE_CONTENT便XXX
*/
// 文本格式数组
private final String [] TEXT_FORMAT;
// 格式模板索引常量
private static final int FORMAT_FOLDER_NAME = 0;
private static final int FORMAT_NOTE_DATE = 1;
private static final int FORMAT_NOTE_CONTENT = 2;
// 上下文用于访问ContentResolver、资源文件
private Context mContext;
private String mFileName; // 导出文件的名称如notes_20251223.txt
private String mFileDirectory; // 导出文件的目录路径(如/sdcard/MiNotes/
private String mFileName;
private String mFileDirectory;
/**
*
* @param context
*
*
* @param context
*/
public TextExport(Context context) {
// 从资源文件加载导出文本的格式模板
TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
mContext = context;
// 初始化文件名/目录为空,导出成功后赋值
mFileName = "";
mFileDirectory = "";
}
/**
*
* @param id FORMAT_FOLDER_NAME/FORMAT_NOTE_DATE/FORMAT_NOTE_CONTENT
* @return
* ID
*
* @param id ID
* @return
*/
private String getFormat(int id) {
return TEXT_FORMAT[id];
}
/**
* 便
* @param folderId ID
* @param ps SD
* 便
*
* @param folderId ID
* @param ps
*/
private void exportFolderToText(String folderId, PrintStream ps) {
// 查询该文件夹下的所有普通便签
// 查询该文件夹下的所有便签
Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
NoteColumns.PARENT_ID + "=?", // 查询条件父文件夹ID匹配
new String[] { folderId },
null);
NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] {
folderId
}, null);
if (notesCursor != null) {
// 遍历文件夹下的所有便签
if (notesCursor.moveToFirst()) {
do {
// 1. 打印便签最后修改时间(按格式模板)
// 打印便签最后修改时间
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm), // 时间格式:月日时分
mContext.getString(R.string.format_datetime_mdhm),
notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 2. 导出单条便签的详细内容
// 查询属于该便签的数据
String noteId = notesCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (notesCursor.moveToNext());
}
// 关闭游标,释放数据库资源
notesCursor.close();
}
}
/**
* 便
* 便便
* @param noteId 便ID便
* @param ps SD
* 便
*
* @param noteId 便ID
* @param ps
*/
private void exportNoteToText(String noteId, PrintStream ps) {
// 查询该便签的具体数据(内容/通话记录详情)
// 查询该便签的所有数据
Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
DATA_PROJECTION,
DataColumns.NOTE_ID + "=?", // 查询条件便签ID匹配
new String[] { noteId },
null);
DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] {
noteId
}, null);
if (dataCursor != null) {
// 遍历便签的所有数据项(单条便签可能包含多个数据项,如通话记录+附件)
if (dataCursor.moveToFirst()) {
do {
String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
// 1. 处理通话记录类型便签
if (DataConstants.CALL_NOTE.equals(mimeType)) {
// 获取通话记录核心信息
// 打印电话号码
String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER);
long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE);
String location = dataCursor.getString(DATA_COLUMN_CONTENT);
// 打印手机号(非空时)
if (!TextUtils.isEmpty(phoneNumber)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
phoneNumber));
}
// 打印通话时间
// 打印通话日期
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat
.format(mContext.getString(R.string.format_datetime_mdhm), callDate)));
// 打印通话记录附件位置(非空时)
.format(mContext.getString(R.string.format_datetime_mdhm),
callDate)));
// 打印通话附件位置
if (!TextUtils.isEmpty(location)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
location));
}
}
// 2. 处理普通文本便签
else if (DataConstants.NOTE.equals(mimeType)) {
} else if (DataConstants.NOTE.equals(mimeType)) {
// 打印便签内容
String content = dataCursor.getString(DATA_COLUMN_CONTENT);
// 打印便签内容(非空时)
if (!TextUtils.isEmpty(content)) {
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT),
content));
}
}
} while (dataCursor.moveToNext());
}
// 关闭游标,释放资源
dataCursor.close();
}
// 便签内容结束后,添加分隔符(换行+分隔符),区分不同便签
// 打印便签之间的分隔线
try {
ps.write(new byte[] {
Character.LINE_SEPARATOR, Character.LETTER_NUMBER
Character.LINE_SEPARATOR, Character.LINE_SEPARATOR
});
} catch (IOException e) {
// 写入分隔符失败时输出错误日志,不中断整体导出流程
Log.e(TAG, e.toString());
}
}
/**
* 便SD
*
* 1. SD
* 2.
* 3. +
* 4. 便
* 5.
* @return BackupUtilsSTATE_XXX
* 便
* <p>
*
* 1. SD
* 2.
* 3. 便
* 4. 便
*
* @return STATE_*
*/
public int exportToText() {
// 步骤1检查SD卡是否挂载未挂载直接返回对应状态码
if (!externalStorageAvailable()) {
Log.d(TAG, "Media was not mounted");
return STATE_SD_CARD_UNMOUONTED;
}
// 步骤2创建导出文件的打印流失败则返回系统错误
PrintStream ps = getExportToTextPrintStream();
if (ps == null) {
Log.e(TAG, "get print stream error");
return STATE_SYSTEM_ERROR;
}
// 步骤3导出所有有效文件夹排除回收站+ 通话记录文件夹
// 导出文件夹及其便签
Cursor folderCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
// 查询条件:
// - 类型为文件夹 + 父文件夹不是回收站;
// - 或ID为通话记录文件夹特殊系统文件夹
"(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
+ NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER,
null,
null);
+ NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null);
if (folderCursor != null) {
if (folderCursor.moveToFirst()) {
do {
// 处理文件夹名称:通话记录文件夹使用固定名称,其他文件夹用摘要
// 打印文件夹名称
String folderName = "";
if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) {
folderName = mContext.getString(R.string.call_record_folder_name);
} else {
folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET);
}
// 打印文件夹名称(非空时)
if (!TextUtils.isEmpty(folderName)) {
ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
}
// 导出该文件夹下的所有便签
String folderId = folderCursor.getString(NOTE_COLUMN_ID);
exportFolderToText(folderId, ps);
} while (folderCursor.moveToNext());
@ -377,66 +326,53 @@ public class BackupUtils {
folderCursor.close();
}
// 步骤4导出根目录下的普通便签父文件夹ID为0
// 导出根目录下的便签
Cursor noteCursor = mContext.getContentResolver().query(
Notes.CONTENT_NOTE_URI,
NOTE_PROJECTION,
// 查询条件:类型为普通便签 + 父文件夹为根目录
NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID
+ "=0",
null,
null);
+ "=0", null, null);
if (noteCursor != null) {
if (noteCursor.moveToFirst()) {
do {
// 打印便签修改时间
ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format(
mContext.getString(R.string.format_datetime_mdhm),
noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
// 导出单条便签内容
// 查询属于该便签的数据
String noteId = noteCursor.getString(NOTE_COLUMN_ID);
exportNoteToText(noteId, ps);
} while (noteCursor.moveToNext());
}
noteCursor.close();
}
// 步骤5关闭打印流释放文件资源
ps.close();
// 导出成功,返回成功状态码
return STATE_SUCCESS;
}
/**
* SDPrintStream
* generateFileMountedOnSDcardPrintStream
* @return PrintStream/null
*
*
* @return
*/
private PrintStream getExportToTextPrintStream() {
// 生成SD卡上的导出文件带日期的文本文件
File file = generateFileMountedOnSDcard(mContext, R.string.file_path,
R.string.file_name_txt_format);
if (file == null) {
Log.e(TAG, "create file to exported failed");
return null;
}
// 记录导出文件的名称和目录(供外部获取)
mFileName = file.getName();
mFileDirectory = mContext.getString(R.string.file_path);
PrintStream ps = null;
try {
// 创建文件输出流,包装为打印流(方便文本写入)
FileOutputStream fos = new FileOutputStream(file);
ps = new PrintStream(fos);
} catch (FileNotFoundException e) {
// 文件未找到异常打印堆栈返回null
e.printStackTrace();
return null;
} catch (NullPointerException e) {
// 空指针异常打印堆栈返回null
e.printStackTrace();
return null;
}
@ -445,48 +381,40 @@ public class BackupUtils {
}
/**
* SD
* notes_20251223.txt便
* @param context
* @param filePathResId IDR.string.file_path/MiNotes/
* @param fileNameFormatResId IDR.string.file_name_txt_formatnotes_%s.txt
* @return File/null
*
*
* @param context
* @param filePathResId ID
* @param fileNameFormatResId ID
* @return
*/
private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
StringBuilder sb = new StringBuilder();
// 1. 拼接SD卡根目录 + 预设目录路径(如/sdcard/MiNotes/
sb.append(Environment.getExternalStorageDirectory());
sb.append(context.getString(filePathResId));
File filedir = new File(sb.toString());
// 2. 拼接完整文件路径:目录 + 带日期的文件名(如/sdcard/MiNotes/notes_20251223.txt
sb.append(context.getString(
fileNameFormatResId,
// 格式化文件名中的日期部分(年-月-日)
DateFormat.format(context.getString(R.string.format_date_ymd),
System.currentTimeMillis())));
File file = new File(sb.toString());
try {
// 3. 创建目录(不存在时)
if (!filedir.exists()) {
filedir.mkdir();
}
// 4. 创建文件(不存在时)
if (!file.exists()) {
file.createNewFile();
}
// 5. 返回创建成功的文件对象
return file;
} catch (SecurityException e) {
// 权限异常如无SD卡写入权限打印堆栈返回null
e.printStackTrace();
} catch (IOException e) {
// IO异常如创建文件失败打印堆栈返回null
e.printStackTrace();
}
// 创建失败返回null
return null;
}
}
}

@ -0,0 +1,105 @@
/*
* 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;
/**
* 便
* <p>
* 便便
* 便便
* <p>
* [2025 ]: AI便
* AI
*/
public class CategoryUtil {
// 分类关键词映射表,存储关键词与对应分类的映射关系
private static final Map<String, String> CATEGORY_KEYWORDS = new HashMap<>();
// 静态初始化块,初始化分类关键词
static {
// 工作相关关键词
addKeywords("工作", "工作", "任务", "项目", "会议", "报告", "加班", "上班", "下班", "同事", "领导", "客户", "公司");
// 学习相关关键词
addKeywords("学习", "学习", "考试", "作业", "复习", "预习", "课程", "老师", "学生", "学校", "教材", "笔记", "知识点");
// 生活相关关键词
addKeywords("生活", "生活", "日常", "购物", "吃饭", "旅游", "电影", "音乐", "健身", "运动", "休息", "睡觉", "起床");
// 想法相关关键词
addKeywords("想法", "想法", "创意", "灵感", "思考", "感悟", "心得", "体会", "观点", "意见", "建议");
// 待办相关关键词
addKeywords("待办", "待办", "todo", "需要", "必须", "应该", "计划", "安排", "准备");
// 其他默认分类
addKeywords("其他", "");
}
/**
*
*
* @param category
* @param keywords
*/
private static void addKeywords(String category, String... keywords) {
for (String keyword : keywords) {
CATEGORY_KEYWORDS.put(keyword, category);
}
}
/**
* 便
* <p>
* 便便
*
* @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 "其他";
}
}

@ -5,7 +5,7 @@
* 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
* 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,
@ -34,18 +34,25 @@ import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
import java.util.HashSet;
/**
*
*
*
* <p>
* 便
* UIContentResolverContentProvider
* 便
*/
public class DataUtils {
public static final String TAG = "DataUtils";
/**
*
* @param resolver ContentResolver
* @param ids ID
* @return truefalse
* 便
* <p>
* ContentProvider便
*
* @param resolver ContentResolverContentProvider
* @param ids 便ID
* @return
*/
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
@ -57,23 +64,19 @@ public class DataUtils {
return true;
}
// 批量操作列表,用于执行事务性删除
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
// 保护系统根文件夹不被删除
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue;
}
// 构建删除操作根据URI删除指定ID的笔记
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
operationList.add(builder.build());
}
try {
// 批量执行删除操作
// DB操作批量删除便签
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
// 检查结果是否有效
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
@ -88,47 +91,65 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param id ID
* @param srcFolderId ID
* 便
*
* @param resolver ContentResolver
* @param id 便ID
* @param srcFolderId ID
* @param desFolderId ID
*/
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId); // 设置新的父文件夹
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId); // 记录原始父文件夹
values.put(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
values.put(NoteColumns.PARENT_ID, desFolderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
// DB操作更新便签的文件夹信息
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
/**
*
* @param resolver ContentResolver
* @param ids ID
* 便
* <p>
* 便
*
* @param resolver ContentResolver
* @param ids 便ID
* @param folderId ID
* @return truefalse
* @return
*/
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;
}
// 批量操作列表,用于执行事务性更新
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
// 构建更新操作,修改笔记的父文件夹
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId); // 设置新的父文件夹
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); // 标记为本地已修改
// 如果是移动到回收站保存原始父文件夹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());
}
try {
// 批量执行移动操作
// DB操作批量更新便签的文件夹信息
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
@ -144,12 +165,13 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @return
*
*
* @param resolver ContentResolver
* @return
*/
public static int getUserFolderCount(ContentResolver resolver) {
// 查询类型为文件夹且父ID不是回收站的记录数
// DB操作查询用户文件夹数量
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
@ -160,11 +182,11 @@ public class DataUtils {
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0); // 获取第一列的计数结果
count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
cursor.close(); // 确保游标被关闭
cursor.close();
}
}
}
@ -172,14 +194,15 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param noteId ID
* @param type
* @return truefalse
* 便
*
* @param resolver ContentResolver
* @param noteId 便ID
* @param type 便
* @return
*/
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
// 查询指定ID和类型的笔记且父ID不是回收站
// DB操作查询便签是否存在且未被删除到回收站
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
@ -188,7 +211,7 @@ public class DataUtils {
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) { // 检查是否有匹配的记录
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -197,19 +220,20 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
* @param noteId ID
* @return truefalse
* 便
*
* @param resolver ContentResolver
* @param noteId 便ID
* @return
*/
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
// 查询指定ID的笔记不做其他条件限制
// DB操作查询便签是否存在
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) { // 检查是否有匹配的记录
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -218,19 +242,20 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
*
*
* @param resolver ContentResolver
* @param dataId ID
* @return truefalse
* @return
*/
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
// 查询指定ID的数据记录
// DB操作查询数据是否存在
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
boolean exist = false;
if (cursor != null) {
if (cursor.getCount() > 0) { // 检查是否有匹配的记录
if (cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -239,21 +264,22 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
*
*
* @param resolver ContentResolver
* @param name
* @return truefalse
* @return
*/
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
// 查询类型为文件夹、不在回收站中且名称匹配的记录
// DB操作查询文件夹名称是否已存在
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) {
if(cursor.getCount() > 0) { // 检查是否有同名文件夹
if(cursor.getCount() > 0) {
exist = true;
}
cursor.close();
@ -262,13 +288,14 @@ public class DataUtils {
}
/**
*
* @param resolver ContentResolver
*
*
* @param resolver ContentResolver
* @param folderId ID
* @return
*/
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
// 查询文件夹下所有笔记的小部件ID和类型
// DB操作查询文件夹中的小部件信息
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
@ -281,10 +308,9 @@ public class DataUtils {
set = new HashSet<AppWidgetAttribute>();
do {
try {
// 封装小部件属性对象
AppWidgetAttribute widget = new AppWidgetAttribute();
widget.widgetId = c.getInt(0); // 小部件ID
widget.widgetType = c.getInt(1); // 小部件类型
widget.widgetId = c.getInt(0);
widget.widgetType = c.getInt(1);
set.add(widget);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, e.toString());
@ -297,13 +323,14 @@ public class DataUtils {
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
* 便ID
*
* @param resolver ContentResolver
* @param noteId 便ID
* @return
*/
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
// 查询指定笔记的通话号码限定MIME类型为通话记录
// DB操作查询通话记录的电话号码
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?",
@ -312,36 +339,37 @@ public class DataUtils {
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0); // 获取通话号码
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
cursor.close(); // 确保游标被关闭
cursor.close();
}
}
return "";
}
/**
* ID
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return ID0
* 便ID
*
* @param resolver ContentResolver
* @param phoneNumber
* @param callDate
* @return 便ID
*/
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
// 使用自定义的PHONE_NUMBERS_EQUAL函数比较号码确保同一联系人的不同格式号码能匹配
// DB操作查询通话记录的便签ID
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);
if (cursor != null) {
if (cursor.moveToFirst()) {
try {
return cursor.getLong(0); // 获取笔记ID
return cursor.getLong(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call note id fails " + e.toString());
}
@ -352,14 +380,14 @@ public class DataUtils {
}
/**
* ID
* @param resolver ContentResolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
* 便ID便
*
* @param resolver ContentResolver
* @param noteId 便ID
* @return 便
*/
public static String getSnippetById(ContentResolver resolver, long noteId) {
// 查询指定ID笔记的摘要字段
// DB操作查询便签摘要
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
NoteColumns.ID + "=?",
@ -369,27 +397,94 @@ public class DataUtils {
if (cursor != null) {
String snippet = "";
if (cursor.moveToFirst()) {
snippet = cursor.getString(0); // 获取摘要内容
snippet = cursor.getString(0);
}
cursor.close();
return snippet;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
// 如果找不到noteId返回空字符串而不是抛出异常
return "";
}
/**
*
* 便
* <p>
*
*
* @param snippet
* @return
*/
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim(); // 去除首尾空格
snippet = snippet.trim();
int index = snippet.indexOf('\n');
if (index != -1) {
snippet = snippet.substring(0, index); // 只保留第一行
snippet = snippet.substring(0, index);
}
}
return snippet;
}
}
/**
* ID
*
* @param resolver ContentResolver
* @param folderName
* @return ID0
*/
public static long getFolderIdByName(ContentResolver resolver, String folderName) {
// DB操作根据文件夹名称查询文件夹ID
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;
}
/**
*
* <p>
* ID
*
* @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);
// DB操作插入新文件夹
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;
}
}

@ -14,159 +14,123 @@
* limitations under the License.
*/
// 包声明归属小米便签的工具模块定义Google TasksGTask同步相关的核心字符串常量
package net.micode.notes.tool;
/**
* Google TasksGTask
*
* 1. 便GTaskJSON
* 2. GTaskMIUI便
* 3. GTask便
* 便GTask/
* GTask
* <p>
* GTask JSON GTask
* </p>
*/
public class GTaskStringUtils {
// ======================== GTask同步JSON交互 - 动作相关字段 ========================
/** JSON字段动作ID标识单次同步操作的唯一ID */
/**
* GTask JSON
*/
// 动作相关字段
public final static String GTASK_JSON_ACTION_ID = "action_id";
/** JSON字段动作列表批量同步时存储多个动作的数组 */
public final static String GTASK_JSON_ACTION_LIST = "action_list";
/** JSON字段动作类型标识当前同步动作的类型如创建/查询/移动/更新) */
public final static String GTASK_JSON_ACTION_TYPE = "action_type";
/** JSON字段动作类型-创建同步时向GTask创建新任务/文件夹) */
public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";
/** JSON字段动作类型-全量查询从GTask拉取所有任务/文件夹数据) */
public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";
/** JSON字段动作类型-移动将GTask任务/文件夹移动到指定目录) */
public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";
/** JSON字段动作类型-更新更新GTask中已存在的任务/文件夹信息) */
public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update";
// ======================== GTask同步JSON交互 - 实体/用户相关字段 ========================
/** JSON字段创建者ID标识GTask实体的创建者账号ID */
// 创建者相关字段
public final static String GTASK_JSON_CREATOR_ID = "creator_id";
/** JSON字段子实体存储GTask文件夹下的子任务/子文件夹) */
// 子实体相关字段
public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";
/** JSON字段客户端版本标识便签客户端的版本号用于GTask服务端兼容 */
// 客户端版本字段
public final static String GTASK_JSON_CLIENT_VERSION = "client_version";
/** JSON字段完成状态标识GTask任务是否已完成布尔值 */
// 完成状态字段
public final static String GTASK_JSON_COMPLETED = "completed";
/** JSON字段当前列表ID标识任务/文件夹所属的GTask列表ID */
// 列表相关字段
public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";
/** JSON字段默认列表IDGTask默认任务列表的ID */
public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id";
/** JSON字段删除标记标识GTask实体是否已被删除布尔值 */
// 删除相关字段
public final static String GTASK_JSON_DELETED = "deleted";
/** JSON字段目标列表移动操作时的目标列表ID */
// 移动相关字段
public final static String GTASK_JSON_DEST_LIST = "dest_list";
/** JSON字段目标父节点移动操作时的目标父实体ID */
public final static String GTASK_JSON_DEST_PARENT = "dest_parent";
/** JSON字段目标父节点类型移动操作时目标父实体的类型GROUP/TASK */
public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type";
/** JSON字段实体增量同步时仅传输实体的变更部分减少数据传输 */
// 实体相关字段
public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";
/** JSON字段实体类型标识GTask实体的类型任务/文件夹) */
public final static String GTASK_JSON_ENTITY_TYPE = "entity_type";
/** JSON字段获取已删除项同步时是否拉取GTask中已删除的实体 */
// 获取删除相关字段
public final static String GTASK_JSON_GET_DELETED = "get_deleted";
/** JSON字段实体IDGTask任务/文件夹的唯一标识ID */
// ID 相关字段
public final static String GTASK_JSON_ID = "id";
/** JSON字段索引GTask实体在父节点中的排序索引 */
public final static String GTASK_JSON_INDEX = "index";
/** JSON字段最后修改时间GTask实体的最后修改时间戳 */
// 修改时间相关字段
public final static String GTASK_JSON_LAST_MODIFIED = "last_modified";
/** JSON字段最新同步点标识上次同步的位置用于增量同步 */
// 同步相关字段
public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";
/** JSON字段列表IDGTask列表的唯一标识ID */
// 列表相关字段
public final static String GTASK_JSON_LIST_ID = "list_id";
/** JSON字段列表数组存储GTask所有列表的数组 */
public final static String GTASK_JSON_LISTS = "lists";
/** JSON字段名称GTask任务/文件夹的名称) */
// 名称相关字段
public final static String GTASK_JSON_NAME = "name";
/** JSON字段新ID创建/移动操作后生成的新实体ID */
// 新 ID 字段
public final static String GTASK_JSON_NEW_ID = "new_id";
/** JSON字段备注GTask任务的备注内容对应便签的正文 */
// 便签相关字段
public final static String GTASK_JSON_NOTES = "notes";
/** JSON字段父节点IDGTask实体的父文件夹/父任务ID */
// 父级相关字段
public final static String GTASK_JSON_PARENT_ID = "parent_id";
/** JSON字段前序兄弟ID标识实体在父节点中的前一个兄弟实体ID用于排序 */
// 排序相关字段
public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";
/** JSON字段同步结果GTask服务端返回的同步操作结果 */
// 结果相关字段
public final static String GTASK_JSON_RESULTS = "results";
/** JSON字段源列表移动操作时的源列表ID */
// 源列表字段
public final static String GTASK_JSON_SOURCE_LIST = "source_list";
/** JSON字段任务数组存储GTask所有任务的数组 */
// 任务相关字段
public final static String GTASK_JSON_TASKS = "tasks";
/** JSON字段类型兼容字段同entity_type */
// 类型相关字段
public final static String GTASK_JSON_TYPE = "type";
/** JSON字段类型-分组GTask文件夹的类型标识 */
public final static String GTASK_JSON_TYPE_GROUP = "GROUP";
/** JSON字段类型-任务GTask普通任务的类型标识 */
public final static String GTASK_JSON_TYPE_TASK = "TASK";
/** JSON字段用户标识GTask所属的用户账号信息 */
// 用户相关字段
public final static String GTASK_JSON_USER = "user";
// ======================== GTask侧文件夹命名规则 ========================
/** MIUI便签专属文件夹前缀区分GTask中其他文件夹避免命名冲突 */
// MIUI 文件夹前缀
public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]";
/** GTask侧默认文件夹名称对应便签的根目录 */
// 默认文件夹
public final static String FOLDER_DEFAULT = "Default";
/** GTask侧通话记录文件夹名称对应便签的通话记录文件夹 */
// 通话便签文件夹
public final static String FOLDER_CALL_NOTE = "Call_Note";
/** GTask侧元数据文件夹名称存储便签与GTask的同步映射元数据 */
// 元数据文件夹
public final static String FOLDER_META = "METADATA";
// ======================== 同步元数据标识字段 ========================
/** 元数据头-GTask ID存储便签对应的GTask实体ID */
// 元数据头信息
public final static String META_HEAD_GTASK_ID = "meta_gid";
/** 元数据头-便签(存储便签核心信息的元数据标识) */
public final static String META_HEAD_NOTE = "meta_note";
/** 元数据头-数据(存储便签扩展数据的元数据标识) */
public final static String META_HEAD_DATA = "meta_data";
/** 元数据便签名称GTask中元数据便签的固定名称禁止修改/删除) */
// 元数据便签名称
public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";
}
}

@ -5,7 +5,7 @@
* 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
* 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,
@ -24,36 +24,44 @@ import net.micode.notes.ui.NotesPreferenceActivity;
/**
*
* ID
*
* <p>
* 便
* 便
*/
public class ResourceParser {
// 背景颜色常量定义
/**
*
*/
public static final int YELLOW = 0;
public static final int BLUE = 1;
public static final int WHITE = 2;
public static final int GREEN = 3;
public static final int RED = 4;
// 默认背景颜色
/**
*
*/
public static final int BG_DEFAULT_COLOR = YELLOW;
// 字体大小常量定义
/**
*
*/
public static final int TEXT_SMALL = 0;
public static final int TEXT_MEDIUM = 1;
public static final int TEXT_LARGE = 2;
public static final int TEXT_SUPER = 3;
// 默认字体大小
/**
*
*/
public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;
/**
*
*
* 便
*/
public static class NoteBgResources {
// 编辑界面背景资源数组,索引对应颜色常量
// 编辑界面背景资源数组
private final static int [] BG_EDIT_RESOURCES = new int [] {
R.drawable.edit_yellow,
R.drawable.edit_blue,
@ -62,7 +70,7 @@ public class ResourceParser {
R.drawable.edit_red
};
// 编辑界面标题背景资源数组
// 编辑界面标题背景资源数组
private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] {
R.drawable.edit_title_yellow,
R.drawable.edit_title_blue,
@ -72,8 +80,9 @@ public class ResourceParser {
};
/**
* ID
* @param id
* 便
*
* @param id ID
* @return ID
*/
public static int getNoteBgResource(int id) {
@ -81,9 +90,10 @@ public class ResourceParser {
}
/**
* ID
* @param id
* @return ID
* 便
*
* @param id ID
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
return BG_EDIT_TITLE_RESOURCES[id];
@ -92,28 +102,26 @@ public class ResourceParser {
/**
* ID
*
* @param context
* @return
* <p>
* ID
*
* @param context
* @return ID
*/
public static int getDefaultBgId(Context context) {
// 检查用户是否启用了随机背景色设置
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) {
// 生成随机颜色索引
return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length);
} else {
// 返回默认黄色
return BG_DEFAULT_COLOR;
}
}
/**
*
*
* 便
*/
public static class NoteItemBgResources {
// 列表项背景资源数组
// 列表项第一项背景资源数组
private final static int [] BG_FIRST_RESOURCES = new int [] {
R.drawable.list_yellow_up,
R.drawable.list_blue_up,
@ -122,7 +130,7 @@ public class ResourceParser {
R.drawable.list_red_up
};
// 列表中间项背景资源数组
// 列表中间项背景资源数组
private final static int [] BG_NORMAL_RESOURCES = new int [] {
R.drawable.list_yellow_middle,
R.drawable.list_blue_middle,
@ -131,7 +139,7 @@ public class ResourceParser {
R.drawable.list_red_middle
};
// 列表项背景资源数组
// 列表项最后一项背景资源数组
private final static int [] BG_LAST_RESOURCES = new int [] {
R.drawable.list_yellow_down,
R.drawable.list_blue_down,
@ -140,7 +148,7 @@ public class ResourceParser {
R.drawable.list_red_down,
};
// 列表单一项背景资源数组(只有一项时使用)
// 列表一项背景资源数组
private final static int [] BG_SINGLE_RESOURCES = new int [] {
R.drawable.list_yellow_single,
R.drawable.list_blue_single,
@ -150,8 +158,9 @@ public class ResourceParser {
};
/**
* ID
* @param id
*
*
* @param id ID
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
@ -159,8 +168,9 @@ public class ResourceParser {
}
/**
* ID
* @param id
*
*
* @param id ID
* @return ID
*/
public static int getNoteBgLastRes(int id) {
@ -168,8 +178,9 @@ public class ResourceParser {
}
/**
* ID
* @param id
*
*
* @param id ID
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
@ -177,8 +188,9 @@ public class ResourceParser {
}
/**
* ID
* @param id
*
*
* @param id ID
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
@ -186,7 +198,8 @@ public class ResourceParser {
}
/**
* ID
*
*
* @return ID
*/
public static int getFolderBgRes() {
@ -195,8 +208,7 @@ public class ResourceParser {
}
/**
*
* 2x24x4
*
*/
public static class WidgetBgResources {
// 2x2小部件背景资源数组
@ -209,8 +221,9 @@ public class ResourceParser {
};
/**
* 2x2ID
* @param id
* 2x2
*
* @param id ID
* @return ID
*/
public static int getWidget2xBgResource(int id) {
@ -227,8 +240,9 @@ public class ResourceParser {
};
/**
* 4x4ID
* @param id
* 4x4
*
* @param id ID
* @return ID
*/
public static int getWidget4xBgResource(int id) {
@ -237,11 +251,10 @@ public class ResourceParser {
}
/**
*
*
*
*/
public static class TextAppearanceResources {
// 字体大小样式资源数组,索引对应字体大小常量
// 文本外观资源数组
private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] {
R.style.TextAppearanceNormal,
R.style.TextAppearanceMedium,
@ -250,27 +263,30 @@ public class ResourceParser {
};
/**
* ID
* @param id
* @return ID
*
*
* @param id ID
* @return ID
*/
public static int getTexAppearanceResource(int id) {
/**
* HACKME: IDSharedPreferencebug
* ID
* HACKME: Fix bug of store the resource id in shared preference.
* The id may larger than the length of resources, in this case,
* return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE}
*/
if (id >= TEXTAPPEARANCE_RESOURCES.length) {
return BG_DEFAULT_FONT_SIZE; // 越界时返回默认值
return BG_DEFAULT_FONT_SIZE;
}
return TEXTAPPEARANCE_RESOURCES[id];
}
/**
*
* @return
*
*
* @return
*/
public static int getResourcesSize() {
return TEXTAPPEARANCE_RESOURCES.length;
}
}
}
}

@ -0,0 +1,221 @@
/*
* 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;
/**
*
* <p>
*
* SharedPreferences
*
*/
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;
/**
*
*
* @param context
*/
private UserManager(Context context) {
mContext = context.getApplicationContext();
mPrefs = mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
/**
* UserManager
* <p>
* UserManager
*
* @param context
* @return UserManager
*/
public static synchronized UserManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new UserManager(context);
}
return sInstance;
}
/**
*
* <p>
* IDSharedPreferences
*
* @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;
}
/**
*
* <p>
* 访ID
*
* @param userId ID
* @param password
* @return
*/
public boolean validatePassword(long userId, String password) {
try {
// DB操作验证用户密码
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;
}
}
/**
*
* <p>
* ID
*
* @param userId ID
*/
public void setCurrentUser(long userId) {
try {
// DB操作查询用户的用户名
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();
}
}
}
}

@ -14,260 +14,223 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块便签提醒功能的最终展示页是提醒触发时用户感知的核心页面
package net.micode.notes.ui;
// 安卓页面核心基类,所有页面的父类,提供页面生命周期、窗口管理、组件交互等基础能力
import android.app.Activity;
// 安卓系统对话框核心类,用于展示标准化的弹窗,承载提醒内容与交互按钮,是本页面的核心展示载体
import android.app.AlertDialog;
// 安卓上下文核心类,提供系统服务获取、资源访问、组件通信等基础能力
import android.content.Context;
// 安卓对话框事件回调相关类,处理按钮点击、对话框关闭等交互事件,实现页面的核心交互逻辑
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
// 安卓意图核心类用于页面跳转、数据传递此处用于跳转便签编辑页并携带便签ID
import android.content.Intent;
// 安卓音频管理核心类,用于指定音频流类型,适配系统音量与静音规则,控制提醒铃声的播放策略
import android.media.AudioManager;
// 安卓媒体播放核心类,用于加载、播放、停止系统铃声资源,实现提醒音效的播放功能
import android.media.MediaPlayer;
// 安卓铃声管理核心类用于获取系统默认的闹钟铃声Uri统一访问系统铃声资源
import android.media.RingtoneManager;
// 安卓统一资源标识类,用于标记铃声资源地址、便签数据的唯一标识
import android.net.Uri;
// 安卓页面状态保存类,用于横竖屏切换等场景的页面数据恢复,本页面未使用
import android.os.Bundle;
// 安卓电源管理核心类,用于获取设备屏幕的亮灭状态,判断当前是亮屏/熄屏/锁屏状态
import android.os.PowerManager;
// 安卓系统设置核心类,用于读取系统的铃声静音模式配置,适配不同的系统音频策略
import android.provider.Settings;
// 安卓窗口管理相关类,用于配置页面窗口的显示属性,实现锁屏显示、屏幕唤醒、常亮等核心功能
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
// 小米便签资源常量类引用字符串、布局、颜色等本地资源统一管理资源ID
import net.micode.notes.R;
// 便签应用核心数据常量类定义便签类型、ContentURI等全局常量用于数据合法性校验
import net.micode.notes.data.Notes;
// 便签数据工具类,封装数据库查询相关的通用方法,提供便签摘要查询、便签存在性校验等能力
import net.micode.notes.tool.DataUtils;
// Java IO异常类捕获媒体播放器加载铃声资源时的IO错误保证程序健壮性
import java.io.IOException;
/**
* 便
* Activity
* OnClickListener + OnDismissListener
* 便便
* ++便使
*
*
* 1. //
* 2. /穿
* 3. 使
* 4.
*
* 1. Intent便IDIDAlarmReceiver
* 2. 便ID便
* 3. 60
* 4. 便便便
*
* 1. Uri使
* 2.
* 3.
* 4.
* 5.
* 便
* 1. 便
* 2. +便/
* 3. 便便
* 4.
* &
* 1. 穿
* 2.
* 3.
* 4. 使
* 5.
*
* AlarmManager广 AlarmReceiver广 +便 便 + + / 便
*
* - 便便/
* -
* -
* 便
* 便
* 便
*
*
* - ActivityOnClickListenerOnDismissListener
* -
* -
* - 便
* - Android
*
*
* - 便
* -
* - 便
* -
* -
*/
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
// ======================== 成员变量区 ========================
/** 当前提醒绑定的便签唯一ID核心标识所有数据操作均基于此ID */
/**
* 便ID
*/
private long mNoteId;
/** 便签的文本摘要内容,用于弹窗展示,做了长度限制处理 */
/**
* 便
*/
private String mSnippet;
/** 便签摘要预览的最大字符长度,超过该长度自动裁剪并添加省略号,保证展示美观 */
/**
*
*/
private static final int SNIPPET_PREW_MAX_LEN = 60;
/** 媒体播放器实例,用于加载和播放系统闹钟铃声,实现听觉提醒 */
MediaPlayer mPlayer;
/**
*
*
* @param savedInstanceState 使
*
*/
MediaPlayer mPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 核心配置:隐藏原生的页面标题栏,本页面仅展示对话框,无需标题栏,极简展示
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); // 保持屏幕常亮,直到弹窗关闭
win.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); // 强制唤醒熄屏的屏幕,核心保障用户能看到提醒
win.addFlags(WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);// 允许屏幕在亮屏状态下依然保持锁定,兼顾安全性
win.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); // 适配系统的装饰布局,避免弹窗内容被遮挡
}
// ========== 第二步解析Intent数据获取便签ID并查询摘要内容 ==========
Intent intent = getIntent();
try {
// 核心解析逻辑从Intent的Data字段中解析出便签IDData字段的格式为 content://notes/note/[id]
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 根据便签ID查询数据库获取便签的文本摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 摘要内容规范化处理超过60字符则截取前60位并添加省略号保证弹窗展示效果
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) {
// 异常处理ID解析失败如格式错误、数据异常打印日志并直接返回不展示任何内容
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)) {
showActionDialog(); // 展示提醒弹窗,核心交互载体
playAlarmSound(); // 播放系统闹钟铃声,核心听觉提醒
if (mNoteId > 0 && DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
showActionDialog();
playAlarmSound();
} else {
// 便签已被删除或类型非法:直接关闭页面,无任何展示与播放,避免无效操作
finish();
}
}
/**
*
* @return boolean true=/ false=/
*/
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();
}
}
/**
*
* Uri
*/
private void playAlarmSound() {
// 第一步获取系统默认的闹钟铃声Uri使用系统级铃声资源适配用户个性化设置
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 {
mPlayer.setDataSource(this, url); // 设置铃声资源的数据源
mPlayer.prepare(); // 同步准备播放器,加载铃声资源
mPlayer.setLooping(true); // 核心配置:循环播放铃声,直到用户手动关闭
mPlayer.start(); // 启动铃声播放,触发听觉提醒
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
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 (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); // 弹窗核心内容:便签的文本摘要,展示提醒的核心信息
dialog.setPositiveButton(R.string.notealert_ok, this); // 确认按钮,绑定点击事件监听器
// 智能按钮适配:仅在屏幕亮屏时展示「进入便签」按钮,熄屏/锁屏时不展示,贴合操作场景
dialog.setTitle(R.string.app_name);
// 如果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);
}
// 展示弹窗并绑定关闭监听器,无论何种方式关闭弹窗,均触发统一的收尾逻辑
dialog.show().setOnDismissListener(this);
}
/**
*
* @param dialog
* @param which 便
*/
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:
// 点击「进入便签」按钮:跳转至该便签的编辑页面,精准定位到对应内容
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW); // 设置跳转动作:查看/编辑便签
intent.putExtra(Intent.EXTRA_UID, mNoteId); // 传递便签ID目标页面根据ID查询并展示内容
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, mNoteId);
startActivity(intent);
break;
default:
// 点击「确认」按钮无额外业务逻辑仅关闭弹窗收尾逻辑由OnDismissListener统一处理
break;
}
}
/**
*
*
* @param dialog
*/
public void onDismiss(DialogInterface dialog) {
stopAlarmSound(); // 核心收尾:停止铃声播放并释放媒体资源,杜绝内存泄漏与音频残留
finish(); // 关闭当前页面,释放所有资源,页面生命周期结束
stopAlarmSound();
finish();
}
/**
*
*
*/
private void stopAlarmSound() {
if (mPlayer != null) {
mPlayer.stop(); // 立即停止铃声播放
mPlayer.release(); // 释放播放器的所有资源,包括音频通道、内存等
mPlayer = null; // 置空引用便于GC回收彻底杜绝内存泄漏
try {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
} catch (Exception e) {
// 捕获所有异常,避免崩溃
e.printStackTrace();
mPlayer = null;
}
}
}
}
}

@ -14,129 +14,86 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块便签提醒功能的初始化核心广播接收器负责重启恢复所有未过期提醒
package net.micode.notes.ui;
// 安卓系统闹钟服务核心类,用于注册、设置、取消定时闹钟,是便签提醒的核心调度组件
import android.app.AlarmManager;
// 安卓延迟意图核心类,封装广播/页面跳转意图交由AlarmManager在指定时间触发核心桥梁类
import android.app.PendingIntent;
// 安卓广播核心基类,继承此类实现广播监听与处理能力,为本类的核心父类
import android.content.BroadcastReceiver;
// 安卓ContentURI拼接工具类用于将Uri和数据ID拼接生成唯一标识Uri便于数据精准匹配
import android.content.ContentUris;
// 安卓上下文核心类,提供系统服务获取、内容解析器访问、组件通信等基础能力
import android.content.Context;
// 安卓意图核心类,组件间通信的核心载体,用于封装广播意图并携带数据
import android.content.Intent;
// 安卓数据库游标核心类,用于遍历查询数据库返回的结果集,读取便签提醒数据
import android.database.Cursor;
// 便签应用核心数据常量类定义数据库Uri、便签类型、字段名等全局常量统一管理
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
* 便广
* BroadcastReceiver 广广
* 便/便
* AlarmManager
*
* 1. /广广广广
* 2. 广ContentResolver便
* 3. +便便TYPE_NOTE//
* 4. 广AlarmManager
* 5. 便/
* &
* 1. 便
* 2. 便IDIO
* 3. ContentUris便IDIntentData便
* 4. 使AlarmManager.RTC_WAKEUP/CPU
* 5. Cursor
* 6. 广
*
* -
* - 便TYPE_NOTETYPE_FOLDER
* - 便ALERTED_DATE0便
*
* 便 AlarmManager / 广 AlarmReceiver
* 广
* 便
* 便
*
*
* - BroadcastReceiver广
* - ContentResolver便
* - 便AlarmManager
* - AndroidAlarmManager API
*
*
* -
* - 便
* - 便
* - Android 6.0+setExact
* - Android 12+PendingIntent flag
*/
public class AlarmInitReceiver extends BroadcastReceiver {
/**
*
* IO
*
* 便ID
*/
private static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 数组索引0便签的唯一主键ID用于绑定提醒与便签、生成唯一Intent标识
NoteColumns.ALERTED_DATE // 数组索引1便签设置的提醒时间戳毫秒级用于设置闹钟的触发时间
NoteColumns.ID,
NoteColumns.ALERTED_DATE
};
// 投影数组列索引常量【硬编码优化】
// 设计目的:将数组索引封装为常量,替代代码中的硬编码数字,提升代码可读性、可维护性,避免索引写错导致的程序异常
private static final int COLUMN_ID = 0; // 对应PROJECTION[0]便签ID列索引
private static final int COLUMN_ALERTED_DATE = 1; // 对应PROJECTION[1],提醒时间戳列索引
/**
* 广广
* +
* @param context 广ContentResolverAlarmManagerIntent
* @param intent 广广
* 便ID
*/
private static final int COLUMN_ID = 0;
/**
*
*/
private static final int COLUMN_ALERTED_DATE = 1;
@Override
public void onReceive(Context context, Intent intent) {
// 第一步:获取当前系统时间戳,作为筛选「未过期提醒」的核心阈值,只处理提醒时间在当前时间之后的记录
long currentDate = System.currentTimeMillis();
// 第二步通过ContentResolver查询便签数据库获取所有符合条件的有效提醒数据
// 核心查询参数说明:
// 1. uri查询地址 → Notes.CONTENT_NOTE_URI 普通便签的专属Uri精准定位查询表
// 2. projection查询字段 → 自定义的PROJECTION数组仅查ID和提醒时间戳
// 3. selection查询条件 → 双重过滤:提醒时间>当前时间 且 便签类型为普通便签,精准筛选有效数据
// 4. selectionArgs条件参数 → 传入当前时间戳的字符串形式防止SQL注入符合安卓安全规范
// 5. sortOrder排序规则 → null使用数据库默认排序无需额外排序提升查询效率
Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
PROJECTION,
NoteColumns.ALERTED_DATE + ">? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE,
new String[] { String.valueOf(currentDate) },
null);
// 第三步:遍历查询结果,为每个有效提醒重新注册闹钟
if (c != null) { // 游标非空校验:避免查询结果为空时的空指针异常,代码健壮性保障
// 游标移动到第一条数据,存在有效提醒时进入循环遍历逻辑
if (c != null) {
if (c.moveToFirst()) {
do {
// 3.1 从游标中读取当前行的核心数据便签ID、提醒触发时间戳
long alertDate = c.getLong(COLUMN_ALERTED_DATE); // 该便签的提醒触发时间,毫秒级
long noteId = c.getLong(COLUMN_ID); // 该便签的唯一主键ID
// 3.2 创建广播意图指定意图的目标为AlarmReceiver即提醒时间到达时需要触发的广播接收器
long alertDate = c.getLong(COLUMN_ALERTED_DATE);
Intent sender = new Intent(context, AlarmReceiver.class);
// 核心关键将便签ID拼接至Intent的Data字段生成唯一的Uri标识精准绑定该提醒与对应便签
// 作用AlarmReceiver接收到广播时可通过该Uri解析出便签ID进而查询便签详情展示提醒内容无串号风险
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
// 3.3 创建PendingIntent延迟意图封装上述广播意图交由AlarmManager管理在指定时间触发广播
// PendingIntent.getBroadcast参数说明上下文、请求码(此处传0即可)、待封装的广播意图、flags标记(默认0)
// 核心作用PendingIntent是一种授权意图允许系统在未来的指定时间以当前应用的身份发送该广播是闹钟触发的核心载体
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
// 3.4 获取系统AlarmManager服务实例系统级的闹钟调度核心服务
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 使用适当的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);
// 3.5 核心操作:重新注册闹钟,完成提醒恢复
// alermManager.set参数说明
// 1. type → AlarmManager.RTC_WAKEUP 最核心的闹钟类型基于系统绝对时间触发时唤醒设备CPU/屏幕,保证提醒必达
// 2. triggerAtTime → 闹钟触发的具体时间即从数据库读取的提醒时间戳alertDate
// 3. operation → 待触发的PendingIntent延迟意图触发时发送广播至AlarmReceiver
alermManager.set(AlarmManager.RTC_WAKEUP, alertDate, pendingIntent);
} while (c.moveToNext()); // 循环遍历游标,处理所有有效提醒数据
// 在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();
}
}
}
}

@ -14,50 +14,43 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块便签提醒功能的广播接收核心类承接闹钟触发逻辑
package net.micode.notes.ui;
// 安卓广播核心基类,继承此类实现广播接收能力,监听系统/应用发送的广播事件
import android.content.BroadcastReceiver;
// 安卓上下文核心类,提供应用运行环境、组件启动、资源访问等基础能力
import android.content.Context;
// 安卓意图核心类,用于组件间通信、页面跳转、数据传递的核心载体
import android.content.Intent;
/**
* 便广广
* BroadcastReceiver 广广
* 便
*
* 1. AlarmManager广广便
* 2. 广Intent便便ID
* 3. Intent
* 4. 便
* &
* 1.
* 2. BroadcastReceiver ActivityActivity FLAG_ACTIVITY_NEW_TASK
* 3. 广Intent
* 4. 广onReceive
* 5. 便广广
*
* 便 AlarmManager 广 广 AlarmAlertActivity 便/
* 使便广
* 广
* AlarmManager广AlarmAlertActivity便
*
*
*
* - BroadcastReceiver广
* - 广AlarmAlertActivityIntent
* - FLAG_ACTIVITY_NEW_TASK
* - AlarmAlertActivity
* - 广
*
*
* - 广
* -
* -
*/
public class AlarmReceiver extends BroadcastReceiver {
/**
* 广广
* 广
* @param context 广Activity
* @param intent 广便
*/
@Override
public void onReceive(Context context, Intent intent) {
// 第一步为当前Intent指定跳转的目标页面将广播意图转为页面跳转意图
intent.setClass(context, AlarmAlertActivity.class);
// 第二步添加必选的新任务启动标记解决广播无任务栈启动Activity的上下文问题规避运行时异常
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,438 @@
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;
/**
*
* <p>
*
* NotesDatabaseHelper
*/
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;
}
}
}

@ -14,190 +14,197 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块日期时间选择的核心自定义复合控件供弹窗调用
package net.micode.notes.ui;
// Java文本格式化工具类获取系统的上午/下午文本标识,适配多语言环境
import java.text.DateFormatSymbols;
// Java核心日历工具类全局唯一的时间数据载体处理所有年月日时分的计算、联动、转换逻辑
import java.util.Calendar;
// 小米便签资源文件引用获取布局、字符串等资源ID常量
import net.micode.notes.R;
// 安卓系统核心类 - 上下文,提供控件创建、资源加载、布局填充的运行环境
import android.content.Context;
// 安卓系统日期格式化工具类提供系统24小时制判断、日期文本格式化能力
import android.text.format.DateFormat;
// 安卓视图体系核心类,视图基础属性配置、可见性控制、布局容器核心父类
import android.view.View;
import android.widget.FrameLayout;
// 安卓数字滚轮选择器本控件的核心子组件实现年月日时分的滚轮滑动选择UI
import android.widget.NumberPicker;
/**
* UI
* FrameLayout
* 便DateTimePickerDialog使
* +++
*
* 1. 4NumberPickerUI
* 2. Calendar
* 3. 590+1230+112
* 4. 24/12
* 5. 7. 便
* 6. /
* 7.
* 8.
* 9.
*
* 使DateTimePickerDialog便
*
*
* FrameLayout使NumberPicker
*
*
* - FrameLayoutNumberPicker
* - 使Calendar
* -
* - 2412
* -
* -
*
*
* -
* -
* - 24/12
* - AM/PM12
* -
* -
* -
* -
*/
public class DateTimePicker extends FrameLayout {
// ======================== 基础常量区 - 控件默认配置与固定数值 ========================
/** 控件默认启用状态:初始化时默认所有选择器均可交互 */
/**
*
*/
private static final boolean DEFAULT_ENABLE_STATE = true;
// ======================== 常量区 - 时间单位核心数值 ========================
/** 半天小时数12小时制的核心分界值上午/下午的小时数上限 */
/**
*
*/
private static final int HOURS_IN_HALF_DAY = 12;
/** 全天小时数24小时制的核心数值一天的总小时数 */
/**
*
*/
private static final int HOURS_IN_ALL_DAY = 24;
/** 一周天数:日期选择器固定展示的天数,不可修改,适配短周期提醒业务 */
/**
*
*/
private static final int DAYS_IN_ALL_WEEK = 7;
// ======================== 常量区 - NumberPicker滚轮选择器 取值范围约束 ========================
/** 日期选择器-最小值固定为0对应近7天列表的第一条数据 */
/**
*
*/
private static final int DATE_SPINNER_MIN_VAL = 0;
/** 日期选择器-最大值固定为6对应近7天列表的最后一条数据数值等于天数减1 */
/**
*
*/
private static final int DATE_SPINNER_MAX_VAL = DAYS_IN_ALL_WEEK - 1;
/** 24小时制-小时选择器-最小值凌晨0点时间起点 */
/**
* 24
*/
private static final int HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW = 0;
/** 24小时制-小时选择器-最大值深夜23点时间终点 */
/**
* 24
*/
private static final int HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW = 23;
/** 12小时制-小时选择器-最小值:上午/下午的1点无0点概念 */
/**
* 12
*/
private static final int HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW = 1;
/** 12小时制-小时选择器-最大值:上午/下午的12点封顶数值 */
/**
* 12
*/
private static final int HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW = 12;
/** 分钟选择器-最小值整点0分分钟起点 */
/**
*
*/
private static final int MINUT_SPINNER_MIN_VAL = 0;
/** 分钟选择器-最大值整点前1分钟59分分钟终点 */
/**
*
*/
private static final int MINUT_SPINNER_MAX_VAL = 59;
/** 上下午选择器-最小值0对应上午/AM */
/**
* AM/PM
*/
private static final int AMPM_SPINNER_MIN_VAL = 0;
/** 上下午选择器-最大值1对应下午/PM */
/**
* AM/PM
*/
private static final int AMPM_SPINNER_MAX_VAL = 1;
// ======================== 成员变量区 - UI核心组件【所有子选择器全局持用避免重复查找】 ========================
/** 日期滚轮选择器核心展示近7天的格式化日期文本如「01.15 周四」,支持滑动切换日期 */
/**
*
*/
private final NumberPicker mDateSpinner;
/** 小时滚轮选择器根据24/12小时制展示对应范围的小时数核心时间选择组件 */
/**
*
*/
private final NumberPicker mHourSpinner;
/** 分钟滚轮选择器固定0~59的分钟数选择支持长按快速滚动核心时间选择组件 */
/**
*
*/
private final NumberPicker mMinuteSpinner;
/** 上下午滚轮选择器仅12小时制显示0=上午/AM1=下午/PM适配12小时制的时间展示 */
/**
* AM/PM
*/
private final NumberPicker mAmPmSpinner;
// ======================== 成员变量区 - 核心状态与数据载体【全局核心数据,控件的大脑】 ========================
/** 核心日历对象:全局唯一的时间数据载体,存储当前选中的完整年月日时分信息,所有选择操作最终同步至此,所有外部获取操作均来源于此,保证数据唯一可信 */
/**
*
*/
private Calendar mDate;
/** 日期展示文本数组缓存近7天的格式化日期文本供日期选择器展示使用避免重复计算 */
/**
*
*/
private String[] mDateDisplayValues = new String[DAYS_IN_ALL_WEEK];
/** 上下午状态标记true=上午/AMfalse=下午/PM仅在12小时制下生效控制时间计算与展示 */
/**
*
*/
private boolean mIsAm;
/** 24小时制状态标记true=启用24小时制隐藏上下午选择器false=启用12小时制显示上下午选择器 */
/**
* 使24
*/
private boolean mIs24HourView;
/** 控件整体启用状态标记true=所有选择器可交互false=所有选择器禁用,统一管控交互权限 */
/**
*
*/
private boolean mIsEnabled = DEFAULT_ENABLE_STATE;
/** 初始化状态标记true=控件正在初始化屏蔽所有回调触发false=初始化完成,正常响应所有操作与回调,防止初始化阶段的无效数据通知 */
private boolean mInitialising;
// ======================== 成员变量区 - 回调通信接口 ========================
/** 日期时间变化的回调监听器:外部实现该接口,接收控件的时间变化通知,是控件与外部通信的唯一桥梁 */
private OnDateTimeChangedListener mOnDateTimeChangedListener;
/**
*
*/
private boolean mInitialising;
// ======================== 成员变量区 - 滚轮选择器 值变化监听器【所有选择器的核心交互逻辑,内部闭环】 ========================
/**
*
*
* 77
*
*/
private OnDateTimeChangedListener mOnDateTimeChangedListener;
private NumberPicker.OnValueChangeListener mOnDateChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 计算日期偏移量:新值-旧值 即为需要增减的天数,直接同步至核心日历对象
mDate.add(Calendar.DAY_OF_YEAR, newVal - oldVal);
// 刷新日期选择器的展示文本,保证文本与选中日期一致
updateDateControl();
// 触发全局时间变化回调,向外通知日期已更新
onDateTimeChanged();
}
};
/**
*
* 24/12
*
* 1. 121112 +11211 -11112
* 2. 24230 +1023 -1
* 3. 24
*/
private NumberPicker.OnValueChangeListener mOnHourChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
boolean isDateChanged = false; // 日期是否发生变化的标记位,默认无变化
Calendar cal = Calendar.getInstance(); // 临时日历对象,用于处理日期偏移
// ========== 12小时制 小时边界特殊处理 ==========
boolean isDateChanged = false;
Calendar cal = Calendar.getInstance();
if (!mIs24HourView) {
// 场景1下午状态下小时从11→12触发日期+1跨天
if (!mIsAm && oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
}
// 场景2上午状态下小时从12→11触发日期-1跨天
else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
} else if (mIsAm && oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
// 场景3小时在11和12之间切换时自动翻转上下午状态核心联动逻辑
if (oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY ||
oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1) {
mIsAm = !mIsAm;
updateAmPmControl(); // 同步刷新上下午选择器的选中状态
updateAmPmControl();
}
}
// ========== 24小时制 小时边界特殊处理 ==========
else {
// 场景1小时从23→0触发日期+1跨天一天的结束
} else {
if (oldVal == HOURS_IN_ALL_DAY - 1 && newVal == 0) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, 1);
isDateChanged = true;
}
// 场景2小时从0→23触发日期-1跨天一天的开始
else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
} else if (oldVal == 0 && newVal == HOURS_IN_ALL_DAY - 1) {
cal.setTimeInMillis(mDate.getTimeInMillis());
cal.add(Calendar.DAY_OF_YEAR, -1);
isDateChanged = true;
}
}
// ========== 统一处理将选中的小时数转换为24小时制同步至核心日历对象 ==========
int newHour = mHourSpinner.getValue() % HOURS_IN_HALF_DAY + (mIsAm ? 0 : HOURS_IN_HALF_DAY);
mDate.set(Calendar.HOUR_OF_DAY, newHour);
// 触发全局时间变化回调,向外通知小时已更新
onDateTimeChanged();
// ========== 日期发生变化时,同步更新核心日历对象的年月日 ==========
if (isDateChanged) {
setCurrentYear(cal.get(Calendar.YEAR));
setCurrentMonth(cal.get(Calendar.MONTH));
@ -206,146 +213,82 @@ public class DateTimePicker extends FrameLayout {
}
};
/**
*
*
* 590 +1059 -1
* 12
*/
private NumberPicker.OnValueChangeListener mOnMinuteChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
int minValue = mMinuteSpinner.getMinValue();
int maxValue = mMinuteSpinner.getMaxValue();
int offset = 0; // 小时偏移量,默认无偏移
// 场景1分钟从59→0触发小时+1分钟到顶进位到小时
int offset = 0;
if (oldVal == maxValue && newVal == minValue) {
offset += 1;
}
// 场景2分钟从0→59触发小时-1分钟到底退位到小时
else if (oldVal == minValue && newVal == maxValue) {
} else if (oldVal == minValue && newVal == maxValue) {
offset -= 1;
}
// ========== 小时需要偏移时,执行联动逻辑 ==========
if (offset != 0) {
mDate.add(Calendar.HOUR_OF_DAY, offset); // 同步更新核心日历对象的小时数
mHourSpinner.setValue(getCurrentHour()); // 刷新小时选择器的选中值
updateDateControl(); // 刷新日期选择器,小时偏移可能导致日期变化
// 根据新的小时数,更新上下午状态标记
mDate.add(Calendar.HOUR_OF_DAY, offset);
mHourSpinner.setValue(getCurrentHour());
updateDateControl();
int newHour = getCurrentHourOfDay();
mIsAm = newHour < HOURS_IN_HALF_DAY;
updateAmPmControl(); // 同步刷新上下午选择器的选中状态
if (newHour >= HOURS_IN_HALF_DAY) {
mIsAm = false;
updateAmPmControl();
} else {
mIsAm = true;
updateAmPmControl();
}
}
// ========== 统一处理:将选中的分钟数同步至核心日历对象 ==========
mDate.set(Calendar.MINUTE, newVal);
// 触发全局时间变化回调,向外通知分钟已更新
onDateTimeChanged();
}
};
/**
* 12
* ±12
* +12-12
*/
private NumberPicker.OnValueChangeListener mOnAmPmChangedListener = new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 翻转上下午状态标记
mIsAm = !mIsAm;
// 根据新状态,调整核心日历对象的小时数,保证时间正确
if (mIsAm) {
mDate.add(Calendar.HOUR_OF_DAY, -HOURS_IN_HALF_DAY);
} else {
mDate.add(Calendar.HOUR_OF_DAY, HOURS_IN_HALF_DAY);
}
// 刷新上下午选择器的展示状态
updateAmPmControl();
// 触发全局时间变化回调,向外通知上下午已切换
onDateTimeChanged();
}
};
// ======================== 内部回调接口 - 日期时间变化通知【标准化通信协议】 ========================
/**
*
*
*
*/
public interface OnDateTimeChangedListener {
/**
*
* @param view DateTimePicker
* @param year 2026
* @param month Calendar011112
* @param dayOfMonth 15
* @param hourOfDay 240~23使
* @param minute 0~59
*/
void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute);
}
// ======================== 构造方法区 - 三级重载构造,满足不同初始化需求【核心初始化入口】 ========================
/**
* 1
* 使24
* @param context
*/
public DateTimePicker(Context context) {
this(context, System.currentTimeMillis());
}
/**
* 2
* 24
* @param context
* @param date
*/
public DateTimePicker(Context context, long date) {
this(context, date, DateFormat.is24HourFormat(context));
}
/**
* 3
* ++++++
* @param context
* @param date
* @param is24HourView 24true=false=12
*/
public DateTimePicker(Context context, long date, boolean is24HourView) {
super(context);
mDate = Calendar.getInstance(); // 初始化核心日历对象,默认加载系统当前时间
mInitialising = true; // 标记进入初始化阶段,屏蔽所有回调触发,防止无效数据通知
// 初始化上下午状态标记根据当前小时数判断≥12为下午<12为上午
mDate = Calendar.getInstance();
mInitialising = true;
mIsAm = getCurrentHourOfDay() >= HOURS_IN_HALF_DAY;
// 第一步填充控件的核心布局将xml布局文件加载至当前FrameLayout根容器
inflate(context, R.layout.datetime_picker, this);
// 第二步初始化所有子滚轮选择器绑定控件ID配置基础属性与监听器
// 初始化日期选择器:配置取值范围+绑定值变化监听器
mDateSpinner = (NumberPicker) findViewById(R.id.date);
mDateSpinner.setMinValue(DATE_SPINNER_MIN_VAL);
mDateSpinner.setMaxValue(DATE_SPINNER_MAX_VAL);
mDateSpinner.setOnValueChangedListener(mOnDateChangedListener);
// 初始化小时选择器仅绑定值变化监听器取值范围在24/12小时制设置时动态配置
mHourSpinner = (NumberPicker) findViewById(R.id.hour);
mHourSpinner.setOnValueChangedListener(mOnHourChangedListener);
// 初始化分钟选择器:配置取值范围+设置长按快速滚动间隔+绑定值变化监听器,细节体验优化
mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
mMinuteSpinner.setMinValue(MINUT_SPINNER_MIN_VAL);
mMinuteSpinner.setMaxValue(MINUT_SPINNER_MAX_VAL);
mMinuteSpinner.setLongPressUpdateInterval(100); // 长按滚动间隔100ms滚动更快更流畅
mMinuteSpinner.setOnLongPressUpdateInterval(100);
mMinuteSpinner.setOnValueChangedListener(mOnMinuteChangedListener);
// 初始化上下午选择器:配置取值范围+加载系统原生的AM/PM文本适配多语言+绑定值变化监听器
String[] stringsForAmPm = new DateFormatSymbols().getAmPmStrings();
mAmPmSpinner = (NumberPicker) findViewById(R.id.amPm);
mAmPmSpinner.setMinValue(AMPM_SPINNER_MIN_VAL);
@ -353,85 +296,69 @@ public class DateTimePicker extends FrameLayout {
mAmPmSpinner.setDisplayedValues(stringsForAmPm);
mAmPmSpinner.setOnValueChangedListener(mOnAmPmChangedListener);
// 第三步刷新所有子选择器的初始展示状态保证UI与初始数据一致
// update controls to initial state
updateDateControl();
updateHourControl();
updateAmPmControl();
// 第四步配置24/12小时制状态完成制式适配
set24HourView(is24HourView);
// 第五步:设置控件的初始选中时间,同步至所有选择器与核心日历对象
// set to current time
setCurrentDate(date);
// 第六步:配置控件的整体启用状态,默认启用
setEnabled(isEnabled());
// 第七步:初始化完成,解除初始化标记,控件进入正常工作状态,可响应所有操作与回调
// set the content descriptions
mInitialising = false;
}
// ======================== 重写父类方法 - 控件整体启用/禁用【统一状态管控】 ========================
/**
* FrameLayoutsetEnabled/
*
*
* @param enabled true=false=
*/
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
// 同步所有子选择器的启用状态
mDateSpinner.setEnabled(enabled);
mMinuteSpinner.setEnabled(enabled);
mHourSpinner.setEnabled(enabled);
mAmPmSpinner.setEnabled(enabled);
// 更新全局启用状态标记
mIsEnabled = enabled;
}
/**
* FrameLayoutisEnabled
* @return true=false=
*/
@Override
public boolean isEnabled() {
return mIsEnabled;
}
// ======================== 公共方法区 - 日期时间 取值/赋值 标准化API【外部核心调用接口最全】 ========================
/**
*
* @return
* Get the current date in millis
*
* @return the current date in millis
*/
public long getCurrentDateInTimeMillis() {
return mDate.getTimeInMillis();
}
/**
*
*
* @param date
* Set the current date
*
* @param date The current date in millis
*/
public void setCurrentDate(long date) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(date);
// 解析时间戳为标准化的年月日时分,调用重载方法完成赋值
setCurrentDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH),
cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
}
/**
*
*
* @param year 2026
* @param month Calendar0=1
* @param dayOfMonth 1~31
* @param hourOfDay 240~23
* @param minute 0~59
* Set the current date
*
* @param year The current year
* @param month The current month
* @param dayOfMonth The current dayOfMonth
* @param hourOfDay The current hourOfDay
* @param minute The current minute
*/
public void setCurrentDate(int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
@ -443,17 +370,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @return 2026
* Get current year
*
* @return The current year
*/
public int getCurrentYear() {
return mDate.get(Calendar.YEAR);
}
/**
*
*
* @param year 2026
* Set current year
*
* @param year The current year
*/
public void setCurrentYear(int year) {
if (!mInitialising && year == getCurrentYear()) {
@ -465,17 +393,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @return Calendar011112
* Get current month in the year
*
* @return The current month in the year
*/
public int getCurrentMonth() {
return mDate.get(Calendar.MONTH);
}
/**
*
*
* @param month Calendar0=1
* Set current month in the year
*
* @param month The month in the year
*/
public void setCurrentMonth(int month) {
if (!mInitialising && month == getCurrentMonth()) {
@ -487,17 +416,18 @@ public class DateTimePicker extends FrameLayout {
}
/**
*
* @return 1~31
* Get current day of the month
*
* @return The day of the month
*/
public int getCurrentDay() {
return mDate.get(Calendar.DAY_OF_MONTH);
}
/**
*
*
* @param dayOfMonth 1~31
* Set current day of the month
*
* @param dayOfMonth The day of the month
*/
public void setCurrentDay(int dayOfMonth) {
if (!mInitialising && dayOfMonth == getCurrentDay()) {
@ -509,68 +439,65 @@ public class DateTimePicker extends FrameLayout {
}
/**
* 24
* @return 0~23
* Get current hour in 24 hour mode, in the range (0~23)
* @return The current hour in 24 hour mode
*/
public int getCurrentHourOfDay() {
return mDate.get(Calendar.HOUR_OF_DAY);
}
/**
*
* 24/12使
* @return 240~23121~12
*/
private int getCurrentHour() {
if (mIs24HourView){
return getCurrentHourOfDay();
} else {
int hour = getCurrentHourOfDay();
// 12小时制特殊转换0点→12点13点→1点保证选择器展示正确
return hour > HOURS_IN_HALF_DAY ? hour - HOURS_IN_HALF_DAY : (hour == 0 ? HOURS_IN_HALF_DAY : hour);
if (hour > HOURS_IN_HALF_DAY) {
return hour - HOURS_IN_HALF_DAY;
} else {
return hour == 0 ? HOURS_IN_HALF_DAY : hour;
}
}
}
/**
* 24
* 24/12
*
* @param hourOfDay 240~23
* Set current hour in 24 hour mode, in the range (0~23)
*
* @param hourOfDay
*/
public void setCurrentHour(int hourOfDay) {
if (!mInitialising && hourOfDay == getCurrentHourOfDay()) {
return;
}
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
// 12小时制下的特殊处理转换小时数+更新上下午状态
if (!mIs24HourView) {
if (hourOfDay >= HOURS_IN_HALF_DAY) {
mIsAm = false;
hourOfDay = hourOfDay > HOURS_IN_HALF_DAY ? hourOfDay - HOURS_IN_HALF_DAY : hourOfDay;
if (hourOfDay > HOURS_IN_HALF_DAY) {
hourOfDay -= HOURS_IN_HALF_DAY;
}
} else {
mIsAm = true;
hourOfDay = hourOfDay == 0 ? HOURS_IN_HALF_DAY : hourOfDay;
if (hourOfDay == 0) {
hourOfDay = HOURS_IN_HALF_DAY;
}
}
updateAmPmControl();
}
mHourSpinner.setValue(hourOfDay);
onDateTimeChanged();
}
/**
*
* @return 0~59
* Get currentMinute
*
* @return The Current Minute
*/
public int getCurrentMinute() {
return mDate.get(Calendar.MINUTE);
}
/**
*
*
* @param minute 0~59
* Set current minute
*/
public void setCurrentMinute(int minute) {
if (!mInitialising && minute == getCurrentMinute()) {
@ -581,110 +508,76 @@ public class DateTimePicker extends FrameLayout {
onDateTimeChanged();
}
// ======================== 公共方法区 - 24小时制适配【系统级适配能力】 ========================
/**
* 24
* @return true=24false=12
* @return true if this is in 24 hour view else false.
*/
public boolean is24HourView () {
return mIs24HourView;
}
public boolean is24HourView () {
return mIs24HourView;
}
/**
* 24/12
*
*
* @param is24HourView true=24false=12
* Set whether in 24 hour or AM/PM mode.
*
* @param is24HourView True for 24 hour mode. False for AM/PM mode.
*/
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
// 控制上下午选择器的可见性
public void set24HourView(boolean is24HourView) {
if (mIs24HourView == is24HourView) {
return;
}
mIs24HourView = is24HourView;
mAmPmSpinner.setVisibility(is24HourView ? View.GONE : View.VISIBLE);
int hour = getCurrentHourOfDay();
// 刷新小时选择器的取值范围
updateHourControl();
// 重新适配并设置小时数,保证展示正确
setCurrentHour(hour);
// 刷新上下午选择器的状态
updateAmPmControl();
}
}
// ======================== 内部私有方法区 - 控件UI刷新【核心UI更新逻辑内部闭环】 ========================
/**
*
* 7
* MM.dd EEEE. 01.15
*/
private void updateDateControl() {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mDate.getTimeInMillis());
// 计算近7天的起始日期当前日期向前推4天保证选中日期在列表中间位置交互更友好
cal.add(Calendar.DAY_OF_YEAR, -DAYS_IN_ALL_WEEK / 2 - 1);
mDateSpinner.setDisplayedValues(null); // 清空原有文本,避免残留
// 循环生成7天的格式化日期文本
mDateSpinner.setDisplayedValues(null);
for (int i = 0; i < DAYS_IN_ALL_WEEK; ++i) {
cal.add(Calendar.DAY_OF_YEAR, 1);
mDateDisplayValues[i] = (String) DateFormat.format("MM.dd EEEE", cal);
}
// 将生成的文本数组设置到日期选择器
mDateSpinner.setDisplayedValues(mDateDisplayValues);
// 默认选中列表中间项,即当前日期
mDateSpinner.setValue(DAYS_IN_ALL_WEEK / 2);
mDateSpinner.invalidate(); // 强制刷新UI保证展示生效
mDateSpinner.invalidate();
}
/**
*
* 24UI
*/
private void updateAmPmControl() {
if (mIs24HourView) {
mAmPmSpinner.setVisibility(View.GONE);
} else {
// 设置选中项0=上午/AM1=下午/PM
int index = mIsAm ? Calendar.AM : Calendar.PM;
mAmPmSpinner.setValue(index);
mAmPmSpinner.setVisibility(View.VISIBLE);
}
}
/**
*
* 24
*/
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
private void updateHourControl() {
if (mIs24HourView) {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_24_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_24_HOUR_VIEW);
} else {
mHourSpinner.setMinValue(HOUR_SPINNER_MIN_VAL_12_HOUR_VIEW);
mHourSpinner.setMaxValue(HOUR_SPINNER_MAX_VAL_12_HOUR_VIEW);
}
}
// ======================== 公共方法区 - 回调监听器绑定【通信入口】 ========================
/**
*
* @param callback OnDateTimeChangedListenernull
* Set the callback that indicates the 'Set' button has been pressed.
* @param callback the callback, if null will do nothing
*/
public void setOnDateTimeChangedListener(OnDateTimeChangedListener callback) {
mOnDateTimeChangedListener = callback;
}
// ======================== 内部私有方法区 - 回调触发【核心通信逻辑,内部闭环】 ========================
/**
*
*
* null
*/
private void onDateTimeChanged() {
if (mOnDateTimeChangedListener != null) {
mOnDateTimeChangedListener.onDateTimeChanged(this, getCurrentYear(),
getCurrentMonth(), getCurrentDay(), getCurrentHourOfDay(), getCurrentMinute());
}
}
}
}

@ -14,160 +14,115 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块日期时间选择弹窗的核心封装类集成自定义时间选择控件
package net.micode.notes.ui;
// Java日历工具类核心时间处理载体存储用户选中的年月日时分秒提供时间的赋值与转换能力
import java.util.Calendar;
// 小米便签资源文件引用获取字符串、布局等资源ID常量
import net.micode.notes.R;
// 自定义日期时间选择核心控件本弹窗的核心内容视图提供年月日时分滚轮选择UI
import net.micode.notes.ui.DateTimePicker;
import net.micode.notes.ui.DateTimePicker.OnDateTimeChangedListener;
// 安卓系统弹窗核心类,本类的父类,提供弹窗的基础展示与按钮配置能力
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
// 安卓系统日期格式化工具类提供24小时制判断、时间戳转友好文本的格式化能力
import android.text.format.DateFormat;
import android.text.format.DateUtils;
/**
*
* AlertDialog
* 便便DateTimePicker
* +++API
*
* 1. DateTimePicker
* 2. Calendar
* 3. 24/12
* 4.
* 5.
* 6.
* 7. 便使
* 便便
*
* 便
* AlertDialogDateTimePicker
*
*
* - AlertDialogOnClickListener
* - 使DateTimePicker
* - OnDateTimeSetListener
* - 2412
* -
*
*
* -
* -
* -
* -
* -
* -
*/
public class DateTimePickerDialog extends AlertDialog implements OnClickListener {
// 核心时间数据载体存储用户当前选中的完整日期时间初始化时获取系统当前时间秒数固定置0
/**
*
*/
private Calendar mDate = Calendar.getInstance();
// 24小时制标记位适配系统设置决定时间的展示格式24小时/12小时带上午下午
/**
* 使24
*/
private boolean mIs24HourView;
// 时间选择完成的回调监听器:外部实现该接口接收最终选中的时间戳,核心通信桥梁
/**
*
*/
private OnDateTimeSetListener mOnDateTimeSetListener;
// 自定义日期时间选择控件本弹窗的核心内容视图提供滚轮式年月日时分选择UI业务核心控件
/**
*
*/
private DateTimePicker mDateTimePicker;
/**
*
*
*
*
*/
public interface OnDateTimeSetListener {
/**
*
* @param dialog
* @param date 0
*
* @param dialog
* @param date
*/
void OnDateTimeSet(AlertDialog dialog, long date);
}
/**
*
* +++++
*
* @param context
* @param date
*/
public DateTimePickerDialog(Context context, long date) {
super(context);
// 第一步:创建自定义的日期时间选择控件,作为弹窗的核心内容视图,替换原生弹窗的默认布局
mDateTimePicker = new DateTimePicker(context);
setView(mDateTimePicker);
// 第二步:为时间选择控件绑定实时变化监听器,核心联动逻辑
// 监听用户在滚轮上的每一次选择操作实时同步选中的时间数据到Calendar对象中
mDateTimePicker.setOnDateTimeChangedListener(new OnDateTimeChangedListener() {
public void onDateTimeChanged(DateTimePicker view, int year, int month,
int dayOfMonth, int hourOfDay, int minute) {
// 实时更新核心时间载体将滚轮选中的年月日时分赋值到Calendar对象
mDate.set(Calendar.YEAR, year);
mDate.set(Calendar.MONTH, month);
mDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
mDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
mDate.set(Calendar.MINUTE, minute);
// 实时更新弹窗标题:将最新选中的时间格式化后展示在弹窗顶部,给用户即时反馈
updateTitle(mDate.getTimeInMillis());
}
});
// 第三步:初始化选中的时间数据,设置默认选中的时间戳,统一时间精度
mDate.setTimeInMillis(date);
mDate.set(Calendar.SECOND, 0); // 强制置空秒数,仅保留到分钟级,符合业务使用场景
// 将初始化的时间数据同步到时间选择控件,保证弹窗打开时展示正确的默认时间
mDate.set(Calendar.SECOND, 0);
mDateTimePicker.setCurrentDate(mDate.getTimeInMillis());
// 第四步:配置弹窗的底部按钮,固化交互逻辑,符合用户操作习惯
setButton(context.getString(R.string.datetime_dialog_ok), this); // 确定按钮,绑定当前类的点击事件
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null); // 取消按钮,无业务逻辑,点击仅关闭弹窗
// 第五步适配系统全局的时间显示规则自动获取系统是否开启24小时制保证体验一致性
setButton(context.getString(R.string.datetime_dialog_ok), this);
setButton2(context.getString(R.string.datetime_dialog_cancel), (OnClickListener)null);
set24HourView(DateFormat.is24HourFormat(this.getContext()));
// 第六步:初始化弹窗标题,将默认时间格式化后展示,完成弹窗的最终初始化
updateTitle(mDate.getTimeInMillis());
}
/**
*
* 24
* @param is24HourView true=使24false=使12
*/
public void set24HourView(boolean is24HourView) {
mIs24HourView = is24HourView;
}
/**
*
*
* @param callBack OnDateTimeSetListener
*/
public void setOnDateTimeSetListener(OnDateTimeSetListener callBack) {
mOnDateTimeSetListener = callBack;
}
/**
*
*
*
* @param date
*/
private void updateTitle(long date) {
// 定义时间格式化的标记位组合,指定需要展示的时间维度:年 + 月日 + 时分
int flag =
DateUtils.FORMAT_SHOW_YEAR | // 强制展示年份如「2026年」
DateUtils.FORMAT_SHOW_DATE | // 强制展示日期如「1月15日」
DateUtils.FORMAT_SHOW_TIME; // 强制展示时间如「15:30」或「下午3:30」
// 根据24小时制标记位追加对应的格式化规则自动切换显示格式
DateUtils.FORMAT_SHOW_YEAR |
DateUtils.FORMAT_SHOW_DATE |
DateUtils.FORMAT_SHOW_TIME;
flag |= mIs24HourView ? DateUtils.FORMAT_24HOUR : DateUtils.FORMAT_24HOUR;
// 将时间戳格式化后设置为弹窗的标题文本完成UI更新
setTitle(DateUtils.formatDateTime(this.getContext(), date, flag));
}
/**
*
* OnClickListener
*
* @param arg0
* @param arg1 /
*/
public void onClick(DialogInterface arg0, int arg1) {
// 空值安全校验:避免未绑定回调监听器时触发空指针异常
if (mOnDateTimeSetListener != null) {
// 触发外部回调,传递弹窗实例和最终选中的时间戳,完成时间选择的业务闭环
mOnDateTimeSetListener.OnDateTimeSet(this, mDate.getTimeInMillis());
}
}

@ -14,109 +14,96 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块下拉菜单功能的统一封装工具类全局复用
package net.micode.notes.ui;
// 安卓系统核心类 - 上下文,提供资源加载、控件创建的运行环境,必须依赖项
import android.content.Context;
// 安卓菜单体系核心类Menu管理菜单整体结构MenuItem表示单个菜单项
import android.view.Menu;
import android.view.MenuItem;
// 安卓视图体系核心类,处理视图点击事件、视图基础属性配置
import android.view.View;
import android.view.View.OnClickListener;
// 安卓按钮控件,作为下拉菜单的触发载体,本类核心绑定控件
import android.widget.Button;
// 安卓原生下拉菜单核心控件,实现弹窗式菜单展示,本类封装的核心原生控件
import android.widget.PopupMenu;
// 安卓下拉菜单的菜单项点击事件监听器,监听菜单选项的点击行为
import android.widget.PopupMenu.OnMenuItemClickListener;
// 小米便签资源文件引用获取图标、布局等资源ID常量
import net.micode.notes.R;
/**
* UI
* AndroidPopupMenu+Button
* 便
*
* 1. PopupMenuAPI
* 2. UI
* 3.
* 4. --
* 5. 使
* 便
*
* PopupMenu使
*
*
*
* - ButtonPopupMenu
* -
* -
* -
*
*
* - Button
* -
* -
* -
* -
* -
* - ID
*/
public class DropdownMenu {
// 下拉菜单的触发按钮,全局持用引用:点击该按钮即可弹出下拉菜单,统一配置背景样式
/**
*
*/
private Button mButton;
// Android原生下拉菜单核心控件本类封装的核心对象负责菜单的弹出、收起、承载菜单项
/**
*
*/
private PopupMenu mPopupMenu;
// PopupMenu对应的菜单容器对象用于动态查找、修改、管理菜单项的状态显示/隐藏/禁用等)
/**
*
*/
private Menu mMenu;
/**
*
* +++
*
* @param context PopupMenu
* @param button
* @param menuId IDR.menu.menu_sort
*
* @param context
* @param button
* @param menuId ID
*/
public DropdownMenu(Context context, Button button, int menuId) {
// 保存触发按钮的全局引用,供后续设置文本、复用控件使用
mButton = button;
// 统一设置触发按钮的背景样式:加载内置的下拉箭头图标,保证所有下拉按钮视觉统一
mButton.setBackgroundResource(R.drawable.dropdown_icon);
// 创建原生PopupMenu对象绑定上下文和触发按钮指定菜单弹出的锚点位置
mPopupMenu = new PopupMenu(context, mButton);
// 获取PopupMenu内部的Menu容器对象缓存引用供后续菜单项查找使用避免重复获取
mMenu = mPopupMenu.getMenu();
// 通过菜单解析器将指定的菜单布局资源加载到Menu容器中完成菜单项的初始化展示
mPopupMenu.getMenuInflater().inflate(menuId, mMenu);
// 为触发按钮绑定点击事件监听器:点击按钮时,自动弹出下拉菜单,核心触发逻辑
mButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// 弹出下拉菜单,菜单默认展示在触发按钮的下方,对齐方式由系统原生适配
mPopupMenu.show();
}
});
}
/**
*
*
* ID
* @param listener onMenuItemClick
*
* @param listener
*/
public void setOnDropdownMenuItemClickListener(OnMenuItemClickListener listener) {
// 空值安全校验避免PopupMenu未初始化完成时绑定监听器导致空指针异常
if (mPopupMenu != null) {
// 将外部传入的监听器绑定到PopupMenu所有菜单项的点击事件都会回调该监听器
mPopupMenu.setOnMenuItemClickListener(listener);
}
}
/**
* IDMenuItem
* ///
*
* @param id IDR.id.menu_sort_by_timeR.id.menu_move_folder
* @return MenuItem null
* ID
* @param id ID
* @return
*/
public MenuItem findItem(int id) {
// 直接通过缓存的Menu对象查找菜单项高效无冗余
return mMenu.findItem(id);
}
/**
*
*
*
* @param title
*
* @param title
*/
public void setTitle(CharSequence title) {
// 直接为触发按钮设置文本内容更新UI展示
mButton.setText(title);
}
}
}

@ -14,166 +14,97 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块是文件夹列表展示的核心数据适配器
package net.micode.notes.ui;
// 安卓系统核心类 - 上下文,提供资源加载、视图创建的运行环境
import android.content.Context;
// 安卓数据库游标类,承载文件夹的数据库查询结果集,适配器的核心数据源
import android.database.Cursor;
// 安卓视图体系核心类,用于创建和装载列表子项视图
import android.view.View;
import android.view.ViewGroup;
// 安卓游标适配器基类适配Cursor数据源的列表专用适配器本类核心父类
import android.widget.CursorAdapter;
// 安卓线性布局,作为自定义列表项的根布局容器
import android.widget.LinearLayout;
// 安卓文本控件,用于展示文件夹名称文本内容
import android.widget.TextView;
// 小米便签资源文件引用获取布局、字符串等资源id
import net.micode.notes.R;
// 小米便签核心数据常量定义文件夹id、数据库字段名等全局常量
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
/**
*
* CursorAdapter Cursor
* 便便
*
* 1. IO
* 2. Cursor
* 3.
* 4.
* 5. findViewById
* -
*
* 便
* 便
*
*
* - CursorAdapter使
* - FolderListItem
* -
* -
*
*
* -
* - "父文件夹"
* -
* -
*/
public class FoldersListAdapter extends CursorAdapter {
/**
*
*
*
* ID
*/
public static final String [] PROJECTION = {
NoteColumns.ID, // 数组索引0 - 文件夹的唯一ID数据库主键
NoteColumns.SNIPPET // 数组索引1 - 文件夹的名称,用于列表展示
NoteColumns.ID,
NoteColumns.SNIPPET
};
/**
*
*
* Cursor.getInt(ID_COLUMN) Cursor.getInt(0)
* ID
*/
public static final int ID_COLUMN = 0;
/**
*
*/
public static final int ID_COLUMN = 0; // 文件夹ID对应的游标列索引
public static final int NAME_COLUMN = 1; // 文件夹名称对应的游标列索引
public static final int NAME_COLUMN = 1;
/**
*
* CursorAdapter
* @param context
* @param c ID
*
* @param context
* @param c
*/
public FoldersListAdapter(Context context, Cursor c) {
super(context, c);
}
/**
*
*
* bindView
* @param context
* @param cursor 使
* @param parent ListView
* @return View
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 创建自定义的文件夹列表项视图,内部已完成布局加载和控件绑定
return new FolderListItem(context);
}
/**
*
* /
*
* 1. ID=Notes.ID_ROOT_FOLDER
* 2.
* 3. FolderListItem
* @param view newView
* @param context
* @param cursor
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 视图类型校验,保证类型安全,避免视图强转异常
if (view instanceof FolderListItem) {
// 读取当前游标中的文件夹ID判断是否为根文件夹
long folderId = cursor.getLong(ID_COLUMN);
String folderName;
// 根文件夹特殊处理:替换展示文本;普通文件夹使用原始名称
if (folderId == Notes.ID_ROOT_FOLDER) {
folderName = context.getString(R.string.menu_move_parent_folder);
} else {
folderName = cursor.getString(NAME_COLUMN);
}
// 调用自定义视图的绑定方法,将处理后的文件夹名称设置到文本控件
String folderName = (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
((FolderListItem) view).bind(folderName);
}
}
/**
*
*
* bindView
* @param context
* @param position 0
* @return String
*/
public String getFolderName(Context context, int position) {
// 根据位置获取对应的游标对象,游标已自动定位到对应行
Cursor cursor = (Cursor) getItem(position);
long folderId = cursor.getLong(ID_COLUMN);
// 根文件夹判断与名称适配逻辑与bindView完全一致
return folderId == Notes.ID_ROOT_FOLDER ? context.getString(R.string.menu_move_parent_folder)
: cursor.getString(NAME_COLUMN);
return (cursor.getLong(ID_COLUMN) == Notes.ID_ROOT_FOLDER) ? context
.getString(R.string.menu_move_parent_folder) : cursor.getString(NAME_COLUMN);
}
/**
*
* LinearLayout 线
*
* bind
* 访private 使
*/
private class FolderListItem extends LinearLayout {
// 列表项核心控件:展示文件夹名称的文本控件,全局缓存避免重复查找
private TextView mName;
/**
*
* +
* @param context
*/
public FolderListItem(Context context) {
super(context);
// 将文件夹列表项的布局文件填充到当前的LinearLayout根布局中
inflate(context, R.layout.folder_list_item, this);
// 查找并缓存文件夹名称文本控件,后续直接复用,提升性能
mName = (TextView) findViewById(R.id.tv_folder_name);
}
/**
*
*
* @param name /
*/
public void bind(String name) {
mName.setText(name);
}
}
}
}

@ -0,0 +1,212 @@
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;
/**
*
* <p>
*
* NotesDatabaseHelper
*/
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,264 @@
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;
/**
* 便
* 便便
* FriendNoteListActivity便
*
*
* - Activity使TextView
* - 便
* - 便
* - Intent便IDID
* -
*
*
* - 便
* - 便
* -
* - 便
* - 便//
*/
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,305 @@
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;
/**
* 便
* 便便
* 便
*
*
* - Activity使ListView便
* - NoteAdapter
* - ContentResolver便
* - 便
* -
*
*
* - 便
* -
* - 便
* -
* - 便
*/
public class FriendNoteListActivity extends Activity {
/**
* 便
*/
private ListView mNoteListView;
/**
* 便
*/
private NoteAdapter mNoteAdapter;
/**
* 便
*/
private List<Note> mNoteList;
/**
* ID
*/
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,327 @@
/*
* 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;
/**
*
* <p>
*
* NotesDatabaseHelper
*/
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();
}
}
}

@ -14,264 +14,177 @@
* limitations under the License.
*/
// 包声明归属小米便签的UI模块该类是便签编辑页的核心自定义输入控件
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;
// 安卓属性集类承载布局xml中配置的控件属性自定义View必备构造参数
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
// 安卓日志工具类:输出编辑框相关的调试日志,便于问题排查
import android.util.Log;
// 安卓上下文菜单类:构建长按文本弹出的菜单容器
import android.view.ContextMenu;
// 安卓按键事件类:封装物理按键/软键盘按键的事件信息,用于监听回车、删除键
import android.view.KeyEvent;
// 安卓菜单项类:上下文菜单中的单个选项对象
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
// 安卓触摸事件类:封装屏幕触摸的坐标、动作类型,用于重写触摸定位光标逻辑
import android.view.MotionEvent;
// 安卓原生编辑框本自定义控件的父类继承所有原生EditText基础能力
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
// 小米便签资源类:引用项目中的字符串、图片等资源文件,此处核心用链接菜单的文本资源
import net.micode.notes.R;
// Java集合类HashMap存储链接协议与菜单文本的映射关系实现协议与文案解耦
import java.util.HashMap;
import java.util.Map;
/**
* 便 EditText
* Android EditText便
* Activity
*
* 1. /
* 2.
* 3.
* 4. /
* 5.
* 6. EditText
*
* <p>
* EditText线线
*
*/
public class NoteEditText extends EditText {
// 日志打印的TAG标识固定值便于过滤该控件的所有日志信息
private static final String TAG = "NoteEditText";
// 核心成员变量:当前编辑框在【多编辑框列表】中的索引值
// 作用用于向Activity回调增删操作时标识当前操作的编辑框位置核心支撑多框联动
private int mIndex;
// 核心成员变量:删除键按下瞬间的光标起始位置
// 作用在onKeyDown中记录、onKeyUp中校验判断是否触发「删除当前编辑框」的业务逻辑
private int mSelectionStartBeforeDelete;
// ========== 常量区文本链接的协议头Scheme常量 ==========
// 定义安卓原生支持的三大核心链接协议前缀用于匹配文本中的URLSpan链接类型
private static final String SCHEME_TEL = "tel:"; // 电话链接协议头 → 匹配电话号码链接
private static final String SCHEME_HTTP = "http:"; // 网页链接协议头 → 匹配http/https网页链接
private static final String SCHEME_EMAIL = "mailto:";// 邮箱链接协议头 → 匹配邮件发送链接
private static final String SCHEME_TEL = "tel:" ;
private static final String SCHEME_HTTP = "http:" ;
private static final String SCHEME_EMAIL = "mailto:" ;
/**
* (Scheme) ID
*
*
*/
private static final Map<String, Integer> sSchemaActionResMap = new HashMap<String, Integer>();
static {
// 初始化映射关系:协议头 → 对应的菜单文本资源ID
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); // 电话链接 → 展示「拨打电话」
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); // 网页链接 → 展示「打开网页」
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); // 邮箱链接 → 展示「发送邮件」
sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel);
sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web);
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
/**
*
* NoteEditActivity
* UI
* Call by the {@link NoteEditActivity} to delete or add edit text
*/
public interface OnTextViewChangeListener {
/**
*
* + +
* @param index
* @param text
* Delete current edit text when {@link KeyEvent#KEYCODE_DEL} happens
* and the text is null
*/
void onEditTextDelete(int index, String text);
/**
*
*
* @param index +1
* @param text
* Add edit text after current edit text when {@link KeyEvent#KEYCODE_ENTER}
* happen
*/
void onEditTextEnter(int index, String text);
/**
* /
* /
* @param index
* @param hasText truefalse
* Hide or show item option when text change
*/
void onTextChange(int index, boolean hasText);
}
// 成员变量回调接口的实例对象由上层Activity通过set方法注入
private OnTextViewChangeListener mOnTextViewChangeListener;
/**
*
* @param context
*/
public NoteEditText(Context context) {
super(context, null);
mIndex = 0; // 默认索引为0代表首个编辑框后续可通过setIndex重新赋值
mIndex = 0;
}
/**
*
* @param index
*/
public void setIndex(int index) {
mIndex = index;
}
/**
*
* @param listener OnTextViewChangeListenerActivity
*/
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
/**
* xml
* View
* @param context
* @param attrs xml
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
/**
*
* View
* @param context
* @param attrs
* @param defStyle ID
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
}
/**
*
* EditText
*
* @param event
* @return boolean
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // 只处理「按下」动作,是触摸的起始事件
// 步骤1计算触摸点的【文本相对坐标】- 修正内边距与滚动偏移的影响
int x = (int) event.getX();
int y = (int) event.getY();
x -= getTotalPaddingLeft(); // 减去左侧总内边距得到文本区域内的X坐标
y -= getTotalPaddingTop(); // 减去顶部总内边距得到文本区域内的Y坐标
x += getScrollX(); // 加上横向滚动偏移,适配文本横向滚动的场景
y += getScrollY(); // 加上纵向滚动偏移,适配文本纵向滚动的场景
// 步骤2通过文本布局对象将坐标转换为文本的行列信息
Layout layout = getLayout(); // 获取当前编辑框的文本布局管理器
int line = layout.getLineForVertical(y); // 根据Y坐标获取触摸到的文本行号
int off = layout.getOffsetForHorizontal(line, x); // 根据行号+X坐标获取该行的字符偏移量
// 步骤3手动设置光标到精准的触摸位置核心修复原生偏移问题
Selection.setSelection(getText(), off);
break;
// 确保获得焦点,无论是否有内容
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (!hasFocus()) {
requestFocus();
}
}
// 调用父类方法处理事件
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 super.onTouchEvent(event);
return handled;
}
/**
* /
*
*
* @param keyCode //
* @param event
* @return boolean false=onKeyUptrue=
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
// 回车键按下有监听器则返回false交给onKeyUp处理分割+新增逻辑,无则走原生逻辑
if (mOnTextViewChangeListener != null) {
return false;
}
break;
case KeyEvent.KEYCODE_DEL:
// 删除键按下记录此时的光标起始位置供onKeyUp校验是否触发删除编辑框逻辑
mSelectionStartBeforeDelete = getSelectionStart();
break;
default:
break;
}
// 调用父类方法:处理所有其他按键的原生逻辑,无侵入式修改
return super.onKeyDown(keyCode, event);
}
/**
*
*
*
* @param keyCode
* @param event
* @return boolean true=false=
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch(keyCode) {
case KeyEvent.KEYCODE_DEL:
// ========== 删除键抬起:处理「删除编辑框」核心逻辑 ==========
if (mOnTextViewChangeListener != null) {
// 触发条件【三重校验,缺一不可】:
// 1. 光标在文本的起始位置02. 当前编辑框不是第一个编辑框索引≠03. 有回调监听器
if (0 == mSelectionStartBeforeDelete && mIndex != 0) {
// 回调上层Activity执行删除当前编辑框的逻辑并传递当前文本内容
mOnTextViewChangeListener.onEditTextDelete(mIndex, getText().toString());
return true; // 消费事件,避免原生删除逻辑执行,防止文本内容错乱
return true;
}
} else {
// 无监听器时打印日志,便于调试排查问题
Log.d(TAG, "OnTextViewChangeListener was not seted");
}
break;
case KeyEvent.KEYCODE_ENTER:
// ========== 回车键抬起:处理「分割文本+新增编辑框」核心逻辑 ==========
if (mOnTextViewChangeListener != null) {
int selectionStart = getSelectionStart(); // 获取当前光标位置
// 步骤1分割文本 → 光标后的内容作为新增编辑框的初始化文本
int selectionStart = getSelectionStart();
String text = getText().subSequence(selectionStart, length()).toString();
// 步骤2更新当前编辑框文本 → 只保留光标前的内容,完成文本分割
setText(getText().subSequence(0, selectionStart));
// 步骤3回调上层Activity → 在当前索引+1的位置新增编辑框并传递分割后的文本
mOnTextViewChangeListener.onEditTextEnter(mIndex + 1, text);
} else {
Log.d(TAG, "OnTextViewChangeListener was not seted");
@ -280,58 +193,33 @@ public class NoteEditText extends EditText {
default:
break;
}
// 调用父类方法:处理其他按键的原生抬起逻辑,兼容所有原生功能
return super.onKeyUp(keyCode, event);
}
/**
*
* /
*
* +
* @param focused true=false=
* @param direction
* @param previouslyFocusedRect
*/
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mOnTextViewChangeListener != null) {
// 核心判断:失焦 并且 文本为空 → 回调隐藏按钮;否则回调显示按钮
if (!focused && TextUtils.isEmpty(getText())) {
mOnTextViewChangeListener.onTextChange(mIndex, false);
} else {
mOnTextViewChangeListener.onTextChange(mIndex, true);
}
}
// 调用父类方法:执行原生的焦点变化逻辑,如光标显示/隐藏、背景色变化等
super.onFocusChanged(focused, direction, previouslyFocusedRect);
}
/**
*
*
*
* //
* @param menu
*/
@Override
protected void onCreateContextMenu(ContextMenu menu) {
// 前置判断:只有文本是【富文本(Spanned)】类型时,才处理链接识别逻辑
if (getText() instanceof Spanned) {
// 步骤1获取当前光标选中的文本区域兼容正选/反选两种情况
int selStart = getSelectionStart();
int selEnd = getSelectionEnd();
int min = Math.min(selStart, selEnd); // 选中区域的起始位置
int max = Math.max(selStart, selEnd); // 选中区域的结束位置
// 步骤2从选中区域中提取所有的URLSpan链接对象
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
final URLSpan[] urls = ((Spanned) getText()).getSpans(min, max, URLSpan.class);
// 步骤3仅处理「单个链接」的场景避免多链接冲突保证菜单的唯一性
if (urls.length == 1) {
int defaultResId = 0; // 初始化菜单文本资源ID
// 步骤4遍历协议映射表匹配当前链接的协议类型获取对应的菜单文本
int defaultResId = 0;
for(String schema: sSchemaActionResMap.keySet()) {
if(urls[0].getURL().indexOf(schema) >= 0) {
defaultResId = sSchemaActionResMap.get(schema);
@ -339,23 +227,217 @@ public class NoteEditText extends EditText {
}
}
// 兜底处理:未匹配到已知协议时,显示「其他链接」的默认文本
if (defaultResId == 0) {
defaultResId = R.string.note_link_other;
}
// 步骤5向菜单中添加自定义选项并绑定点击事件
menu.add(0, 0, 0, defaultResId).setOnMenuItemClickListener(
new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// 核心逻辑触发URLSpan原生的点击事件 → 自动跳转对应链接(拨打电话/打开网页/发送邮件)
// goto a new intent
urls[0].onClick(NoteEditText.this);
return true; // 消费菜单点击事件,避免后续处理
return true;
}
});
}
}
// 必须调用父类方法:无链接时创建原生默认菜单(复制、粘贴、剪切、选择全部等),完整兼容原生功能
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);
}
}

@ -14,372 +14,258 @@
* limitations under the License.
*/
// 包声明归属小米便签UI模块为列表页提供标准化的便签/文件夹数据模型,封装数据库游标解析逻辑
package net.micode.notes.ui;
// 安卓上下文:提供系统服务访问能力,用于联系人查询、内容解析器获取
import android.content.Context;
// 安卓数据库游标:承载数据库查询结果集,是本类核心的数据解析源
import android.database.Cursor;
// 安卓文本工具类:提供字符串判空、文本处理等安全操作方法
import android.text.TextUtils;
// 联系人数据工具类:根据手机号匹配系统通讯录中的联系人姓名
import net.micode.notes.data.Contact;
// 便签核心常量类定义便签类型、文件夹ID、字段名等全局常量
import net.micode.notes.data.Notes;
// 便签数据库列名常量:数据库表的字段名称枚举,避免硬编码字符串
import net.micode.notes.data.Notes.NoteColumns;
// 便签数据工具类:提供通话记录手机号查询、数据格式化等通用能力
import net.micode.notes.tool.DataUtils;
/**
* 便便
* MVCModelUI
*
* 1.
* 2. Cursor便/
* 3. 便
* 4. UI
* 5. getter
* + + +
* 便
* <p>
* (NotesListActivity)
* Cursor便
* ID
*/
public class NoteItemData {
/**
*
* IO
* 便/
*/
static final String[] PROJECTION = new String[]{
NoteColumns.ID, // 0: 便签/文件夹的唯一主键ID
NoteColumns.ALERTED_DATE, // 1: 提醒时间戳0代表无提醒
NoteColumns.BG_COLOR_ID, // 2: 背景色ID用于列表项背景着色
NoteColumns.CREATED_DATE, // 3: 创建时间戳UTC毫秒值
NoteColumns.HAS_ATTACHMENT, // 4: 是否包含附件 0-无 1-有(图片/音频等)
NoteColumns.MODIFIED_DATE, // 5: 最后修改时间戳,列表页优先展示该时间
NoteColumns.NOTES_COUNT, // 6: 文件夹内包含的便签数量,仅文件夹类型有效
NoteColumns.PARENT_ID, // 7: 父文件夹ID根目录为Notes.ID_ROOT_FOLDER
NoteColumns.SNIPPET, // 8: 便签内容摘要/文件夹名称,短文本展示用
NoteColumns.TYPE, // 9: 数据类型,区分便签/文件夹/系统项
NoteColumns.WIDGET_ID, // 10: 关联的桌面小部件ID无效则为默认值
NoteColumns.WIDGET_TYPE // 11: 关联小部件尺寸类型 2x/4x
static final String [] PROJECTION = new String [] {
NoteColumns.ID,
NoteColumns.ALERTED_DATE,
NoteColumns.BG_COLOR_ID,
NoteColumns.CREATED_DATE,
NoteColumns.HAS_ATTACHMENT,
NoteColumns.MODIFIED_DATE,
NoteColumns.NOTES_COUNT,
NoteColumns.PARENT_ID,
NoteColumns.SNIPPET,
NoteColumns.TITLE,
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
NoteColumns.PINNED,
NoteColumns.SORT_ORDER,
NoteColumns.LOCKED,
NoteColumns.PUBLIC,
};
/**
* PROJECTION
* +
*
*/
private 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;
private static final int HAS_ATTACHMENT_COLUMN = 4;
private static final int MODIFIED_DATE_COLUMN = 5;
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;
// ===================== 核心业务数据字段 - 与投影数组一一映射 =====================
// 访问规则全部私有仅通过getter方法访问无setter数据只读保证一致性
private long mId; // 便签/文件夹唯一ID数据库主键
private long mAlertDate; // 提醒时间戳,>0表示该便签设置了提醒
private int mBgColorId; // 背景色ID对应预设的颜色值列表项渲染用
private long mCreatedDate; // 便签创建时间戳
private boolean mHasAttachment; // 是否包含附件数据库int转Java布尔值贴合业务语义
private long mModifiedDate; // 最后修改时间戳,列表页展示的核心时间字段
private int mNotesCount; // 文件夹内便签数量仅TYPE_FOLDER类型有效
private long mParentId; // 父文件夹ID通话记录便签固定为通话记录文件夹ID
private String mSnippet; // 便签纯文本摘要/文件夹名称,已清理勾选标记
private int mType; // 数据类型Notes.TYPE_NOTE/文件夹/系统项
private int mWidgetId; // 关联桌面小部件ID无效则为INVALID_APPWIDGET_ID
private int mWidgetType; // 关联小部件尺寸类型2x/4x两种规格
// ===================== 通话记录专属扩展字段 =====================
private String mName; // 通话记录联系人姓名,无则显示手机号
private String mPhoneNumber; // 通话记录对应的手机号,仅通话便签有值
// ===================== 列表位置状态字段 - 纯UI渲染支撑 =====================
// 作用:标记当前项在列表中的位置,用于适配不同的背景样式(圆角/分割线/边距等)
private boolean mIsLastItem; // 是否为列表最后一条数据
private boolean mIsFirstItem; // 是否为列表第一条数据
private boolean mIsOnlyOneItem; // 是否为列表中唯一的一条数据
private boolean mIsOneNoteFollowingFolder; // 文件夹后的唯一一条便签项
private boolean mIsMultiNotesFollowingFolder;// 文件夹后的第一条便签(后续还有更多项)
/**
* Cursor
*
* @param context
* @param cursor /
*/
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;
private static final int HAS_ATTACHMENT_COLUMN = 4;
private static final int MODIFIED_DATE_COLUMN = 5;
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 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;
private int mBgColorId;
private long mCreatedDate;
private boolean mHasAttachment;
private long mModifiedDate;
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;
private boolean mIsLastItem;
private boolean mIsFirstItem;
private boolean mIsOnlyOneItem;
private boolean mIsOneNoteFollowingFolder;
private boolean mIsMultiNotesFollowingFolder;
public NoteItemData(Context context, Cursor cursor) {
// 第一步:解析数据库核心字段,与投影数组索引一一对应,基础数据初始化
mId = cursor.getLong(ID_COLUMN);
mAlertDate = cursor.getLong(ALERTED_DATE_COLUMN);
mBgColorId = cursor.getInt(BG_COLOR_ID_COLUMN);
mCreatedDate = cursor.getLong(CREATED_DATE_COLUMN);
// 数据库存储为int(0/1)转换为业务语义更清晰的boolean类型
mHasAttachment = cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0;
mHasAttachment = (cursor.getInt(HAS_ATTACHMENT_COLUMN) > 0) ? true : false;
mModifiedDate = cursor.getLong(MODIFIED_DATE_COLUMN);
mNotesCount = cursor.getInt(NOTES_COUNT_COLUMN);
mParentId = cursor.getLong(PARENT_ID_COLUMN);
// 解析摘要文本,并清理勾选框标记标签,仅保留纯文本内容,优化展示效果
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) {
// 根据便签ID查询关联的通话手机号
mPhoneNumber = DataUtils.getCallNumberByNoteId(context.getContentResolver(), mId);
// 手机号非空时,查询系统通讯录匹配联系人姓名
if (!TextUtils.isEmpty(mPhoneNumber)) {
mName = Contact.getContact(context, mPhoneNumber);
// 无匹配联系人时,手机号作为联系人名称兜底展示
if (mName == null) {
mName = mPhoneNumber;
}
}
}
// 第三步:空值兜底处理,防止空指针异常,生产级代码必备容错逻辑
if (mName == null) {
mName = "";
}
// 第四步自动判断当前项在列表中的位置状态为UI渲染提供数据支撑
checkPostion(cursor);
}
/**
* Cursor
*
* Cursor
* @param cursor
*/
private void checkPostion(Cursor cursor) {
// 初始化基础位置状态:首项、尾项、唯一项
mIsLastItem = cursor.isLast();
mIsFirstItem = cursor.isFirst();
mIsOnlyOneItem = cursor.getCount() == 1;
// 初始化文件夹子项状态为默认值false
mIsLastItem = cursor.isLast() ? true : false;
mIsFirstItem = cursor.isFirst() ? true : false;
mIsOnlyOneItem = (cursor.getCount() == 1);
mIsMultiNotesFollowingFolder = false;
mIsOneNoteFollowingFolder = false;
// 核心业务判断:仅处理【普通便签】且【非列表首项】的场景
if (mType == Notes.TYPE_NOTE && !mIsFirstItem) {
// 记录当前游标位置,用于后续回位,防止位置丢失
int currentPosition = cursor.getPosition();
// 游标上移一行,判断上一项是否为【文件夹】或【系统项】
int position = cursor.getPosition();
if (cursor.moveToPrevious()) {
int prevItemType = cursor.getInt(TYPE_COLUMN);
if (prevItemType == Notes.TYPE_FOLDER || prevItemType == Notes.TYPE_SYSTEM) {
// 上一项是文件夹/系统项 → 当前项是文件夹的子项第一条
if (cursor.getCount() > currentPosition + 1) {
// 后续还有更多数据 → 标记为多子项的第一条
if (cursor.getInt(TYPE_COLUMN) == Notes.TYPE_FOLDER
|| cursor.getInt(TYPE_COLUMN) == Notes.TYPE_SYSTEM) {
if (cursor.getCount() > (position + 1)) {
mIsMultiNotesFollowingFolder = true;
} else {
// 后续无数据 → 标记为文件夹后的唯一子项
mIsOneNoteFollowingFolder = true;
}
}
// 游标回位到原位置,必须执行,否则会导致后续数据解析错位
if (!cursor.moveToNext()) {
// 回位失败时主动抛异常快速定位问题避免隐性bug
throw new IllegalStateException("cursor move to previous but can't move back");
}
}
}
}
/**
* 便
* @return true- false-
*/
public boolean isOneFollowingFolder() {
return mIsOneNoteFollowingFolder;
}
/**
* 便
* @return true- false-
*/
public boolean isMultiFollowingFolder() {
return mIsMultiNotesFollowingFolder;
}
/**
*
* @return true- false-
*/
public boolean isLast() {
return mIsLastItem;
}
/**
* 访
* @return /
*/
public String getCallName() {
return mName;
}
/**
*
* @return true- false-
*/
public boolean isFirst() {
return mIsFirstItem;
}
/**
*
* @return true- false-
*/
public boolean isSingle() {
return mIsOnlyOneItem;
}
/**
* 访便/ID
* @return IDlong
*/
public long getId() {
return mId;
}
/**
* 访
* @return UTC0
*/
public long getAlertDate() {
return mAlertDate;
}
/**
* 访
* @return UTC
*/
public long getCreatedDate() {
return mCreatedDate;
}
/**
* 便/
* @return true- false-
*/
public boolean hasAttachment() {
return mHasAttachment;
}
/**
* 访
* @return UTC
*/
public long getModifiedDate() {
return mModifiedDate;
}
/**
* 访ID
* @return ID
*/
public int getBgColorId() {
return mBgColorId;
}
/**
* 访ID
* @return IDID
*/
public long getParentId() {
return mParentId;
}
/**
* 访便
* @return 便
*/
public int getNotesCount() {
return mNotesCount;
}
/**
* getParentId()
*
* @return ID
*/
public long getFolderId() {
public long getFolderId () {
return mParentId;
}
/**
* 访
* @return Notes.TYPE_NOTE//
*/
public int getType() {
return mType;
}
/**
* 访
* @return 2x/4x
*/
public int getWidgetType() {
return mWidgetType;
}
/**
* 访ID
* @return IDINVALID_APPWIDGET_ID
*/
public int getWidgetId() {
return mWidgetId;
}
/**
* 访/
* @return
*/
public int getWidgetType() {
return mWidgetType;
}
public int getWidgetId() {
return mWidgetId;
}
public String getSnippet() {
return mSnippet;
}
/**
* 便
* 0
* @return true- false-
*/
public boolean hasAlert() {
return mAlertDate > 0;
return (mAlertDate > 0);
}
/**
* 便
* +
* @return true-便 false-便/
*/
public boolean isCallRecord() {
return mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber);
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;
}
/**
* Cursor便
*
*
* @param cursor
* @return 便
*/
public static int getNoteType(Cursor cursor) {
return cursor.getInt(TYPE_COLUMN);
}
}
}

@ -14,231 +14,133 @@
* limitations under the License.
*/
// 包声明:小米便签 核心UI模块该包承载应用所有可视化交互页面及配套适配器本类为列表页核心数据适配桥梁
package net.micode.notes.ui;
// -------------------------- 安卓系统核心依赖包 - 上下文/数据库/日志/视图/适配器能力 --------------------------
// 安卓应用全局上下文:提供资源访问、视图创建、数据解析等基础能力,适配器必备依赖
import android.content.Context;
// 安卓数据库游标核心类:封装数据库查询结果集,承载便签数据表的查询数据,列表展示的核心数据来源
import android.database.Cursor;
// 安卓系统日志工具类:输出适配器运行的调试/错误日志,便于问题定位与线上排查
import android.util.Log;
// 安卓视图体系核心类:视图创建、视图容器的核心父类,适配列表项的创建与挂载
import android.view.View;
import android.view.ViewGroup;
// 安卓游标适配器基类专为Cursor数据设计的列表适配器封装数据绑定、视图复用、数据变化监听等核心能力本类的核心父类
import android.widget.CursorAdapter;
// -------------------------- 小米便签业务层核心依赖 - 数据常量/集合工具 --------------------------
// 小米便签数据层核心常量类:定义便签/文件夹/小部件的类型、特殊ID、业务状态等全局核心常量
import net.micode.notes.data.Notes;
// Java集合框架相关类封装选中项状态的存储、遍历、统计支撑批量选择模式的核心逻辑
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
/**
* 便
* 便
* <p>
* {CursorAdapter}MVCUI{NotesListActivity}/Cursor{NotesListItem}
* Cursor
* Cursor///
* 便
* CursorAdapterHashMap
*
* </p>
* 便便
*
*/
public class NotesListAdapter extends CursorAdapter {
/** 日志常量标签:适配器相关日志的统一标识,便于日志过滤与问题定位 */
private static final String TAG = "NotesListAdapter";
/** 应用上下文对象:用于创建列表项视图、解析业务数据,全局复用避免多次创建 */
private Context mContext;
/** 选中项状态映射容器核心数据结构Key=列表项的索引位置Value=该位置的选中状态,支撑选择模式的核心存储 */
private HashMap<Integer, Boolean> mSelectedIndex;
/** 普通便签数量统计:仅统计{Notes.TYPE_NOTE}类型的项,排除文件夹/通话记录等类型,用于全选状态的判定依据 */
private int mNotesCount;
/** 选择模式状态标记true=开启批量选择模式批量操作false=关闭选择模式(普通浏览),控制列表项勾选框的显隐 */
private boolean mChoiceMode;
private String mSearchQuery;
/**
* 便
* 便便
*
*
* <p>
* ID
*/
public static class AppWidgetAttribute {
/** 小部件系统唯一标识ID桌面小部件的注册ID用于精准定位目标小部件 */
public int widgetId;
/** 小部件类型标识区分2x/4x两种尺寸的便签小部件对应{Notes.TYPE_WIDGET_2X}/{Notes.TYPE_WIDGET_4X} */
public int widgetType;
};
/**
*
* 便
* Cursor{changeCursor}
* @param context
*/
public NotesListAdapter(Context context) {
// 父类构造:传入上下文+空CursorCursor数据后续动态绑定保证初始化灵活性
super(context, null);
// 初始化选中项状态映射容器空HashMap保证初始无选中项
mSelectedIndex = new HashMap<Integer, Boolean>();
// 保存应用上下文引用,供后续视图创建与数据解析使用
mContext = context;
// 重置普通便签统计数量为0初始无数据状态
mNotesCount = 0;
mSearchQuery = null;
}
// 设置搜索查询
public void setSearchQuery(String query) {
mSearchQuery = query;
notifyDataSetChanged();
}
/**
*
*
* {NotesListItem}
* @param context
* @param cursor 使
* @param parent ListView
* @return View
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 创建小米便签自定义列表项视图,作为列表的最小展示单元
return new NotesListItem(context);
}
/**
*
*
* Cursor{NoteItemData}
*
* @param view {NotesListItem}
* @param context
* @param cursor 便
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 类型安全校验仅处理自定义的NotesListItem视图防止视图类型错误导致崩溃
if (view instanceof NotesListItem) {
// 将Cursor数据库数据解析为业务数据模型封装所有展示所需的业务字段
NoteItemData itemData = new NoteItemData(context, cursor);
// 调用列表项的核心绑定方法,完成数据渲染+选择模式适配+选中状态赋值
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
isSelectedItem(cursor.getPosition()), mSearchQuery);
}
}
/**
*
* /
*
* @param position
* @param checked true=false=
*/
public void setCheckedItem(final int position, final boolean checked) {
// 更新选中状态映射容器,保存当前位置的选中状态
mSelectedIndex.put(position, checked);
// 通知列表数据发生变化,触发视图刷新,展示最新的选中状态
notifyDataSetChanged();
}
/**
*
* @return boolean true=false=
*/
public boolean isInChoiceMode() {
return mChoiceMode;
}
/**
* /
*
*
* @param mode true=false=
*/
public void setChoiceMode(boolean mode) {
// 清空所有选中项状态,重置选择容器为初始状态
mSelectedIndex.clear();
// 更新选择模式标记,控制后续视图绑定的逻辑分支
mChoiceMode = mode;
}
/**
* /
* {Notes.TYPE_NOTE}便/
*
* @param checked true=false=
*/
public void selectAll(boolean checked) {
// 获取当前绑定的数据库游标,遍历所有列表项数据
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
// 移动游标到当前列表项的位置,匹配对应数据
if (cursor.moveToPosition(i)) {
// 仅处理普通便签类型,过滤文件夹等非选择项
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) {
// 批量设置当前位置的选中状态
setCheckedItem(i, checked);
}
}
}
}
/**
* 便ID
* /便ID
* ID
* @return HashSet<Long> 便ID
*/
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
// 遍历所有已记录的选中状态位置
for (Integer position : mSelectedIndex.keySet()) {
// 仅处理选中状态为true的有效项
if (mSelectedIndex.get(position) == true) {
// 获取当前位置对应的便签ID
Long id = getItemId(position);
// 过滤系统根文件夹的无效ID避免业务操作异常
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
// 将有效ID添加至结果集合
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;
}
/**
*
* 便ID
* null
* @return HashSet<AppWidgetAttribute> null
*/
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
// 遍历所有已记录的选中状态位置
for (Integer position : mSelectedIndex.keySet()) {
// 仅处理选中状态为true的有效项
if (mSelectedIndex.get(position) == true) {
// 获取当前位置对应的数据库游标
Cursor c = (Cursor) getItem(position);
if (c != null) {
// 创建小部件属性对象,封装核心数据
AppWidgetAttribute widget = new AppWidgetAttribute();
// 将游标数据解析为业务模型,提取小部件关联属性
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
// 添加至结果集合
itemSet.add(widget);
/**
* Cursor
* CursorCursorAdapter
* Don't close cursor here, only the adapter could close it
*/
} else {
// 游标无效时输出错误日志返回null标记异常状态
Log.e(TAG, "Invalid cursor");
return null;
}
@ -247,18 +149,11 @@ public class NotesListAdapter extends CursorAdapter {
return itemSet;
}
/**
* 便
* true
* @return int 便0
*/
public int getSelectedCount() {
// 获取所有已记录的选中状态值集合
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
}
// 遍历状态值统计选中状态为true的项数
Iterator<Boolean> iter = values.iterator();
int count = 0;
while (iter.hasNext()) {
@ -269,80 +164,42 @@ public class NotesListAdapter extends CursorAdapter {
return count;
}
/**
*
* 0 便
*
* @return boolean true=便false=//便
*/
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
}
/**
*
*
* @param position
* @return boolean true=false=/
*/
public boolean isSelectedItem(final int position) {
// 状态容器中无该位置记录时,默认返回未选中
if (null == mSelectedIndex.get(position)) {
return false;
}
// 返回该位置的实际选中状态
return mSelectedIndex.get(position);
}
/**
*
* //
* 便
*
*/
@Override
protected void onContentChanged() {
super.onContentChanged();
// 数据变化后重新统计普通便签数量
calcNotesCount();
}
/**
*
*
* 便
* @param cursor
*/
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
// 更换游标后重新统计普通便签数量
calcNotesCount();
}
/**
* 便
* {Notes.TYPE_NOTE}便
*
*/
private void calcNotesCount() {
// 重置计数为0避免累计统计错误
mNotesCount = 0;
// 遍历适配器绑定的所有列表项数据
for (int i = 0; i < getCount(); i++) {
// 获取当前位置对应的数据库游标
Cursor c = (Cursor) getItem(i);
if (c != null) {
// 仅统计普通便签类型的项
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {
// 游标无效时输出错误日志,终止统计避免数据异常
Log.e(TAG, "Invalid cursor");
return;
}
}
}
}
}

@ -14,88 +14,53 @@
* limitations under the License.
*/
// 包声明:小米便签 核心UI模块该包承载应用所有可视化交互页面及自定义UI组件本类为列表页核心子项组件
package net.micode.notes.ui;
// -------------------------- 安卓系统核心依赖包 - 上下文/视图/布局/基础控件能力 --------------------------
// 安卓应用全局上下文:提供资源访问、样式加载、布局渲染等基础能力,自定义控件必备依赖
import android.content.Context;
// 安卓系统时间格式化工具类:提供相对时间格式化能力,将时间戳转为「几分钟前/昨天/上周」等友好展示格式
import android.text.format.DateUtils;
// 安卓视图体系核心父类:控制控件的显示/隐藏、可见性状态、视图属性等核心操作
import android.view.View;
// 安卓复选框控件:列表选择模式下的核心勾选组件,用于便签的批量操作场景
import android.widget.CheckBox;
// 安卓图片展示控件:承载提醒图标、通话记录图标等所有图片类展示内容
import android.widget.ImageView;
// 安卓线性布局容器:本类的父布局,提供横向/纵向的线性控件排列能力,作为列表项的根布局
import android.widget.LinearLayout;
// 安卓文本展示控件:承载所有文本类展示内容,如标题、时间、通话名称等
import android.widget.TextView;
// -------------------------- 小米便签业务层核心依赖 - 资源/数据/工具适配 --------------------------
// 小米便签资源常量类统一管理布局、字符串、样式、图片等所有本地资源ID引用
import net.micode.notes.R;
// 小米便签数据层核心常量类:定义便签/文件夹的类型、特殊ID、业务状态等全局核心常量
import net.micode.notes.data.Notes;
// 小米便签数据格式化工具类:封装便签摘要文本的格式化处理逻辑,统一文本展示规则
import net.micode.notes.tool.DataUtils;
// 小米便签资源解析工具类:封装便签/文件夹的背景资源适配逻辑,提供不同状态的背景资源获取能力
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
/**
* 便
* 便
* <p>
* {LinearLayout}线MVCUI{NotesListActivity}
* 便UI
* 便/UI
* bind
*
* </p>
* 便便
*
*/
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;
/**
*
*
*
* @param context
*/
public NotesListItem(Context context) {
super(context);
// 加载列表项的基础布局文件将xml布局解析为当前线性布局的子视图
inflate(context, R.layout.note_item, this);
// 绑定布局内所有子控件通过ID精准获取并赋值给成员变量
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);
}
/**
*
* //
*
* @param context
* @param data
* @param choiceMode true=false=
* @param checked true=false=
*/
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);
@ -103,106 +68,146 @@ public class NotesListItem extends LinearLayout {
mCheckBox.setVisibility(View.GONE);
}
// 缓存当前绑定的业务数据模型,供后续背景设置与外部数据获取使用
mItemData = data;
// ===== 分支一通话记录专属文件夹系统特殊固定ID独立的展示样式 =====
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 标题展示规则:固定文件夹名称 + 文件夹内的通话记录数量,格式化展示
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
// 展示通话记录专属图标,明确业务类型标识
mAlert.setImageResource(R.drawable.call_record);
}
// ===== 分支二:通话记录子项(归属通话记录文件夹),通话类专属展示样式 =====
else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
} 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);
} else {
mAlert.setVisibility(View.GONE);
}
}
// ===== 分支三:普通业务类型(普通文件夹/普通便签),通用展示样式 =====
else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
// 子分支1普通文件夹类型展示文件夹名称+包含便签数量
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
}
// 子分支2普通便签类型展示便签核心摘要内容
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);
}
/**
*
* 便
* 便///+ ID
* 使
* @param data ID
*/
private void setBackground(NoteItemData data) {
// 获取当前数据模型的背景色标识ID作为背景资源匹配的核心依据
int id = data.getBgColorId();
// 普通便签的背景适配逻辑:多场景精准匹配,保证列表连贯的视觉效果
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
// 场景1列表中唯一项 / 文件夹下的唯一项 → 使用独立完整背景
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
// 场景2列表中的最后一项 → 使用底部收尾样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
// 场景3列表中的第一项 / 文件夹下的多项首项 → 使用顶部起始样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
} else {
// 场景4列表中的中间项 → 使用标准连贯样式背景
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
}
// 文件夹类型统一适配逻辑:所有文件夹(含通话记录文件夹)使用固定背景样式
else {
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
/**
*
* 访//
*
* @return NoteItemData
*/
// 高亮匹配的文本
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;
}
}
}

@ -14,176 +14,184 @@
* limitations under the License.
*/
// 包声明:小米便签 核心UI业务模块承载应用所有可视化交互页面本类为应用设置页核心实现
package net.micode.notes.ui;
// -------------------------- 安卓系统核心依赖包 - 账号/页面/弹窗/广播/数据存储能力 --------------------------
// 安卓账号体系核心类封装Google账号的账号名、账号类型等基础信息载体
import android.accounts.Account;
// 安卓账号管理核心服务类:系统级账号管理器,负责设备上所有账号的查询、管理、鉴权等操作
import android.accounts.AccountManager;
// 安卓顶部导航栏核心类配置页面导航样式、返回按钮、标题等ActionBar相关属性
import android.app.ActionBar;
// 安卓系统弹窗核心类:构建标准化的对话框,承载账号选择、确认提示等交互弹窗
import android.app.AlertDialog;
// 安卓广播核心组件:监听系统/应用内部的广播消息,本类用于监听同步服务状态变更广播
import android.content.BroadcastReceiver;
// 安卓数据封装类封装键值对数据用于ContentProvider执行数据库字段更新操作
import android.content.ContentValues;
// 安卓应用全局上下文:提供资源访问、组件通信、偏好设置读写等核心基础能力
import android.content.Context;
// 安卓对话框交互核心接口:监听弹窗选项的点击事件,处理用户选择逻辑
import android.content.DialogInterface;
// 安卓组件通信核心类:封装页面跳转指令、广播指令、参数传递,实现跨组件通信
import android.content.Intent;
// 安卓广播过滤核心类:筛选需要监听的广播动作,精准匹配目标广播消息
import android.content.IntentFilter;
// 安卓轻量级存储核心类:键值对持久化存储,用于保存应用偏好配置,非数据库存储
import android.content.SharedPreferences;
// 安卓页面状态存储类:保存页面销毁重建时的临时数据,保证页面状态不丢失
import android.os.Bundle;
// 安卓系统偏好设置组件构建设置页面的标准化UI控件封装设置项的展示与交互逻辑
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
// 安卓文本工具类:封装字符串判空、内容对比等常用操作,避免空指针与硬编码判断
import android.text.TextUtils;
// 安卓系统日期格式化工具类:标准化格式化时间戳为指定样式的字符串,适配多语言展示
import android.text.format.DateFormat;
// 安卓布局加载核心类将xml布局文件解析为Java视图对象加载自定义布局与弹窗样式
import android.view.LayoutInflater;
// 安卓页面菜单核心类配置ActionBar右侧菜单的创建与点击事件处理
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;
// -------------------------- 小米便签业务层核心依赖 - 资源/数据/同步服务 --------------------------
// 小米便签资源常量类统一管理布局、字符串、颜色、样式等所有本地资源ID引用
import net.micode.notes.R;
// 小米便签数据层核心常量类定义便签ContentProvider URI、数据表字段、业务常量等核心配置
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
// 小米便签核心同步服务类封装便签与Google Task的双向同步逻辑提供同步启停、状态查询等核心能力
import net.micode.notes.gtask.remote.GTaskSyncService;
/**
* 便 Activity
* <p>
* {PreferenceActivity}MVCUI便
* GoogleGTask
* Google///
* 广便
* SharedPreferences广
* 线线Google
* </p>
* 便
*
*
*
*
* - PreferenceActivity使
* - Google
* -
* -
* -
*
*
* - Google
* -
* -
* -
* -
*/
public class NotesPreferenceActivity extends PreferenceActivity {
/**
* SharedPreferences
*
*/
public static final String PREFERENCE_NAME = "notes_preferences";
/**
* - Google
*
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/**
* -
*
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/**
* - 便便
*
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
/**
* - KeyUI
*
*/
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
public static final String PREFERENCE_PASSWORD_KEY = "pref_key_password";
/**
* - Google
*
*/
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";
/**
*
*/
private PreferenceCategory mAccountCategory;
/**
* 广GTaskSyncService广/UI
*/
private GTaskReceiver mReceiver;
/**
* Google
*/
private Account[] mOriAccounts;
/**
*
*/
private boolean mHasAddedAccount;
/**
*
*
* ActionBar广
*
* @param icicle Bundle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* 配置ActionBar导航启用左上角返回按钮跳转回便签列表页 */
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
// 从xml资源加载设置页面的标准化偏好配置UI结构页面主体内容初始化
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);
registerReceiver(mReceiver, filter);
// 初始化账号缓存数组与状态标记位,默认无账号无新增操作
mOriAccounts = null;
mHasAddedAccount = false;
// 加载自定义页面头部布局并添加至列表顶部,丰富页面视觉层级
View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null);
getListView().addHeaderView(header, null, true);
}
/**
*
*
* GoogleUI
*
*/
@Override
protected void onResume() {
super.onResume();
// 检测添加账号标记位,处理新增账号的自动绑定逻辑
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
// 对比操作前后的账号数量,判断是否有新账号添加成功
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
// 遍历原始账号列表,过滤已存在的账号
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
// 匹配到新增账号,自动完成绑定并终止遍历
if (!found) {
setSyncAccount(accountNew.name);
break;
@ -192,53 +200,36 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
// 统一刷新页面所有UI组件状态保证数据与视图一致
refreshUI();
}
/**
*
* 退
* 广
*/
@Override
protected void onDestroy() {
// 安全注销广播接收器,判空避免空指针异常
if (mReceiver != null) {
unregisterReceiver(mReceiver);
}
super.onDestroy();
}
/**
*
*
*
*/
private void loadAccountPreference() {
// 清空分类容器内原有配置项避免重复添加导致的UI重复展示问题
mAccountCategory.removeAll();
// 新建账号选择偏好配置项,初始化展示文案与交互行为
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
// 绑定配置项点击事件,处理账号选择与变更的核心交互逻辑
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// 同步中禁止账号操作,防止同步数据与账号信息不一致导致异常
if (!GTaskSyncService.isSyncing()) {
if (TextUtils.isEmpty(defaultAccount)) {
// 未绑定账号:展示账号选择弹窗,供用户选择绑定
// the first time to set account
showSelectAccountAlertDialog();
} else {
// 已绑定账号:展示账号变更确认弹窗,提示风险并提供操作选项
// if the account has already been set, we need to promp
// user about the risk
showChangeAccountConfirmAlertDialog();
}
} else {
// 同步中操作拦截,展示友好的吐司提示
Toast.makeText(NotesPreferenceActivity.this,
R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
.show();
@ -247,23 +238,15 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
// 将配置项添加至分类容器完成UI渲染
mAccountCategory.addPreference(accountPref);
}
/**
*
* /
*
*/
private void loadSyncButton() {
// 获取页面同步按钮与同步状态文本控件,完成视图绑定
Button syncButton = (Button) findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// 根据同步服务状态,动态配置按钮行为与文本
// set button state
if (GTaskSyncService.isSyncing()) {
// 同步中状态:按钮文本为取消同步,点击触发同步终止逻辑
syncButton.setText(getString(R.string.preferences_button_sync_cancel));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@ -271,7 +254,6 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
} else {
// 未同步状态:按钮文本为立即同步,点击触发同步启动逻辑
syncButton.setText(getString(R.string.preferences_button_sync_immediately));
syncButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
@ -279,16 +261,13 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
});
}
// 无绑定账号时禁用同步按钮,避免无账号同步的无效操作
syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 配置同步状态文本展示逻辑,区分同步中与历史同步记录
// set last sync time
if (GTaskSyncService.isSyncing()) {
// 同步中:展示实时同步进度文案
lastSyncTimeView.setText(GTaskSyncService.getProgressString());
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 未同步:读取最后同步时间戳,展示格式化的历史同步记录
long lastSyncTime = getLastSyncTime(this);
if (lastSyncTime != 0) {
lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
@ -296,185 +275,137 @@ public class NotesPreferenceActivity extends PreferenceActivity {
lastSyncTime)));
lastSyncTimeView.setVisibility(View.VISIBLE);
} else {
// 无同步记录:隐藏状态文本,简化页面展示
lastSyncTimeView.setVisibility(View.GONE);
}
}
}
/**
* UI
*
*
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
* Google
* Google
*
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载自定义弹窗标题布局,初始化弹窗头部文案
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
// 获取设备上所有Google账号构建账号选择列表
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
// 缓存当前账号列表,用于后续新增账号判断
mOriAccounts = accounts;
mHasAddedAccount = false;
// 存在Google账号时构建单选列表供用户选择
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
// 绑定列表选择事件,选择后完成账号绑定并关闭弹窗
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
// 加载添加新账号的自定义视图,提供账号新增入口
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
// 展示弹窗并绑定添加账号点击事件,跳转系统账号添加页面
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
/**
* /
* //
*
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
// 加载自定义弹窗标题布局,展示当前绑定账号与风险提示
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
// 构建弹窗操作选项,绑定点击事件处理不同操作逻辑
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
// 选择更改账号:跳转账号选择弹窗
showSelectAccountAlertDialog();
} else if (which == 1) {
// 选择移除账号:执行账号解绑逻辑
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* Google
* GoogleGoogle
*
* @return Account[] com.google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
* Google
* 便
*
* @param account Google
*/
private void setSyncAccount(String account) {
// 账号未发生变化时,直接返回避免无效操作
if (!getSyncAccountName(this).equals(account)) {
// 写入偏好配置,持久化存储绑定的账号名
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// 账号变更后清空历史同步时间,保证同步记录的准确性
setLastSyncTime(this, 0);
// 异步线程清理本地便签的同步关联字段,避免主线程阻塞导致页面卡顿
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
// 展示账号绑定成功的友好提示
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
* Google
* 便
* 便
*/
private void removeSyncAccount() {
// 读取偏好配置并清理账号与同步时间相关配置项
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
@ -485,7 +416,7 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
editor.commit();
// 异步线程清理本地便签的同步关联字段,释放系统资源
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
@ -496,24 +427,12 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}).start();
}
/**
* Google
*
* @param context 访
* @return String
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
*
* @param context 访
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
@ -522,37 +441,104 @@ public class NotesPreferenceActivity extends PreferenceActivity {
editor.commit();
}
/**
*
*
* @param context 访
* @return long 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广GTask广
*
* /广UI
* 宿
*/
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 {
/**
* 广广
* 广UI
* @param context 广
* @param intent 广
*/
@Override
public void onReceive(Context context, Intent intent) {
// 刷新页面所有UI控件同步最新状态
refreshUI();
// 同步中状态:更新实时同步进度文本
if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
syncStatus.setText(intent
@ -562,17 +548,9 @@ public class NotesPreferenceActivity extends PreferenceActivity {
}
}
/**
* ActionBar
* 便Activity
*
* @param item
* @return boolean
*/
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// 构建返回主页面的意图,清除顶部栈保证页面唯一性
Intent intent = new Intent(this, NotesListActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
@ -581,4 +559,4 @@ public class NotesPreferenceActivity extends PreferenceActivity {
return false;
}
}
}
}

@ -0,0 +1,62 @@
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;
/**
*
* <p>
* 使Logo
*
*/
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);
}
}

@ -14,88 +14,61 @@
* limitations under the License.
*/
// 包声明:小米便签 桌面小部件功能模块,该包统管所有小部件基类与不同规格的实现子类
package net.micode.notes.widget;
// -------------------------- 安卓系统核心依赖包 - 桌面小部件基础能力 --------------------------
// 安卓延迟意图类:小部件跨进程点击事件核心载体,桌面进程通过该类触发应用内页面跳转,异步执行意图逻辑
import android.app.PendingIntent;
// 安卓系统桌面小部件核心管理类:负责小部件的创建、更新、销毁、状态维护等全生命周期系统调度
import android.appwidget.AppWidgetManager;
// 安卓系统小部件基类:所有桌面小部件的标准父类,封装系统层小部件生命周期回调方法
import android.appwidget.AppWidgetProvider;
// 安卓数据封装类封装ContentProvider的更新数据键值对用于便签数据的字段更新操作
import android.content.ContentValues;
// 安卓应用全局上下文提供资源访问、ContentResolver获取、系统服务调用等核心能力小部件核心依赖
import android.content.Context;
// 安卓组件通信核心类:封装页面跳转指令与传递参数,用于小部件点击后的页面跳转逻辑
import android.content.Intent;
// 安卓数据库游标类承载ContentProvider的查询结果集按需读取便签数据用完需手动释放资源
import android.database.Cursor;
// 安卓系统日志工具类:输出调试与异常日志,便于小部件功能的问题定位与线上排查
import android.util.Log;
// 安卓远程视图类小部件核心UI载体因小部件运行在桌面系统进程需通过该类跨进程渲染UI布局与数据
import android.widget.RemoteViews;
// -------------------------- 小米便签业务层核心依赖 - 资源/数据/页面/工具 --------------------------
// 小米便签资源常量类统一管理布局、字符串、图片、颜色等所有本地资源ID引用
import net.micode.notes.R;
// 小米便签数据层核心常量类定义便签URI、意图参数、业务状态、小部件类型等全局核心常量数据层与UI层通用
import net.micode.notes.data.Notes;
// 小米便签数据表字段子类:简化便签数据库表的列名引用,避免硬编码,提升代码可维护性
import net.micode.notes.data.Notes.NoteColumns;
// 小米便签资源解析工具类:封装多规格小部件背景资源的映射适配逻辑,统一提供不同尺寸的背景资源获取能力
import net.micode.notes.tool.ResourceParser;
// 小米便签核心业务页面:便签新建/编辑页面,小部件点击跳转的核心目标页面
import net.micode.notes.ui.NoteEditActivity;
// 小米便签核心业务页面:便签列表展示页面,隐私模式下小部件点击的跳转目标页面
import net.micode.notes.ui.NotesListActivity;
/**
* 便
* 便
* <p>
* {AppWidgetProvider}MVCUI便
*
* {NoteWidgetProvider_2x}/{NoteWidgetProvider_4x}
* /UI/
*
* </p>
* 便
* AppWidgetProvider
*/
public abstract class NoteWidgetProvider extends AppWidgetProvider {
/**
* 便
*
*
*/
public static final String [] PROJECTION = new String [] {
NoteColumns.ID, // 便签数据表-主键ID唯一标识单条便签数据
NoteColumns.BG_COLOR_ID, // 便签数据表-背景色标识ID用于匹配小部件对应背景资源
NoteColumns.SNIPPET // 便签数据表-内容摘要小部件UI上展示的核心文本内容
NoteColumns.ID,
NoteColumns.BG_COLOR_ID,
NoteColumns.SNIPPET
};
// 投影数组对应的列索引常量固化查询结果集的字段下标简化Cursor取值逻辑避免硬编码索引值导致的错误
public static final int COLUMN_ID = 0; // 投影数组中-便签ID的列索引
public static final int COLUMN_BG_COLOR_ID = 1; // 投影数组中-背景色ID的列索引
public static final int COLUMN_SNIPPET = 2; // 投影数组中-便签摘要的列索引
/**
*
*/
public static final int COLUMN_ID = 0;
public static final int COLUMN_BG_COLOR_ID = 1;
public static final int COLUMN_SNIPPET = 2;
// 日志统一标签:小部件模块所有日志输出的固定标识,便于日志过滤与问题精准定位
private static final String TAG = "NoteWidgetProvider";
/**
*
* 便
* 便便WIDGET_ID
*
* @param context ContentResolver
* @param appWidgetIds ID
*
* <p>
* ID
*
* @param context
* @param appWidgetIds ID
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// 构建数据更新载体:封装需要更新的字段与对应值,将关联标识置为系统无效值
ContentValues values = new ContentValues();
values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
// 遍历所有待删除的小部件ID逐一对关联便签执行数据更新操作
for (int i = 0; i < appWidgetIds.length; i++) {
context.getContentResolver().update(Notes.CONTENT_NOTE_URI,
values,
@ -105,11 +78,13 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
/**
* ID便
* ID + 便便
* @param context ContentResolver
* @param widgetId ID
* @return Cursor 便null
* 便
* <p>
* ContentResolverID便
*
* @param context
* @param widgetId ID
* @return
*/
private Cursor getNoteWidgetInfo(Context context, int widgetId) {
return context.getContentResolver().query(Notes.CONTENT_NOTE_URI,
@ -120,125 +95,105 @@ public abstract class NoteWidgetProvider extends AppWidgetProvider {
}
/**
*
* UI
* @param context
* @param appWidgetManager UI
* @param appWidgetIds ID
*
* <p>
* privacyModeupdateprivacyModefalse
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
update(context, appWidgetManager, appWidgetIds, false);
}
/**
*
* UI
* /便
* @param context 访
* @param appWidgetManager UI
* @param appWidgetIds ID
* @param privacyMode true=()false=()
*
* <p>
* ID便
*
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
* @param privacyMode
*/
private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds,
boolean privacyMode) {
// 遍历所有待更新的小部件ID逐个完成独立的更新逻辑处理
for (int i = 0; i < appWidgetIds.length; i++) {
// 过滤无效的小部件ID跳过无意义的更新操作提升执行效率
if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) {
// 初始化默认背景ID从资源工具类获取应用全局默认的便签背景标识
int bgId = ResourceParser.getDefaultBgId(context);
// 初始化便签摘要:默认空字符串,防止空指针异常
String snippet = "";
// 构建默认跳转意图:无关联便签时,跳转至便签编辑页执行新建操作
Intent intent = new Intent(context, NoteEditActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); // 启动模式:栈顶复用,避免重复创建页面实例
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); // 携带小部件ID参数供编辑页关联使用
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); // 携带小部件类型,由子类实现差异化适配
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]);
intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType());
// 查询当前小部件关联的有效便签数据
Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]);
if (c != null && c.moveToFirst()) {
// 异常日志埋点同一小部件ID关联多条便签数据属于数据异常场景输出错误日志便于排查
if (c.getCount() > 1) {
Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]);
c.close();
return;
}
// 从结果集中解析业务数据,赋值给本地变量用于后续视图渲染
snippet = c.getString(COLUMN_SNIPPET);
bgId = c.getInt(COLUMN_BG_COLOR_ID);
// 携带便签ID参数跳转编辑页时直接定位到当前关联的便签内容
intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID));
// 修改意图动作:由「新建」改为「查看/编辑」已有便签
intent.setAction(Intent.ACTION_VIEW);
} else {
// 无关联便签数据时:展示应用默认的空内容提示文本
snippet = context.getResources().getString(R.string.widget_havenot_content);
// 修改意图动作:执行新建便签的业务逻辑
intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
}
// 安全关闭游标,释放数据库连接资源,防止内存泄漏
if (c != null) {
c.close();
}
// 创建远程视图实例加载子类实现的规格专属布局完成跨进程UI初始化
RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId());
// 为远程视图设置背景资源:加载子类实现的规格专属背景,完成尺寸与背景的精准适配
rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId));
// 携带背景ID参数跳转编辑页时同步使用当前小部件的背景样式
intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId);
/**
*
* PendingIntent
* 便PendingIntent
*/
PendingIntent pendingIntent = null;
if (privacyMode) {
// 隐私模式逻辑:隐藏便签真实内容,展示隐私提示文本
rv.setTextViewText(R.id.widget_text,
context.getString(R.string.widget_under_visit_mode));
// 隐私模式跳转:点击后跳转至便签列表页面,而非编辑页
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent(
context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
} else {
// 普通模式逻辑:展示解析后的便签摘要文本
rv.setTextViewText(R.id.widget_text, snippet);
// 普通模式跳转:点击后跳转至便签编辑页,携带完整业务参数
pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
// 为小部件核心文本区域绑定点击事件,触发上述构建的延迟意图
rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent);
// 通过系统管理器完成最终的UI刷新将渲染完成的远程视图同步至桌面小部件
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
}
}
}
/**
* ID
* /
* @param bgId 便ID
* @return int DrawableID
* ID
* <p>
* IDID
*
* @param bgId ID
* @return ID
*/
protected abstract int getBgResourceId(int bgId);
/**
* ID
* UI
* @return int ID
* ID
*
* @return ID
*/
protected abstract int getLayoutId();
/**
*
*
*
* @return int Notes.TYPE_WIDGET_2X / Notes.TYPE_WIDGET_4X
*
*
* @return
*/
protected abstract int getWidgetType();
}
}

@ -14,7 +14,6 @@
* limitations under the License.
*/
// 包声明:小米便签 桌面小部件功能模块,该包下统管所有尺寸规格的便签桌面小组件实现类
package net.micode.notes.widget;
import android.appwidget.AppWidgetManager;
@ -24,20 +23,22 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.ResourceParser;
/**
* 便 2X
* NoteWidgetProviderMVCUI2X
* 2X
*
* 2x2 便
* <p>
* NoteWidgetProvider2x2便
*
*/
public class NoteWidgetProvider_2x extends NoteWidgetProvider {
/**
*
* 便
* update2X便
* @param context 访
* @param appWidgetManager
* @param appWidgetIds 2XID
*
* <p>
* update
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
@ -45,9 +46,11 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
* 2XID
* 2X便
* @return int 2XIDR.layout.widget_2x
* ID
* <p>
* 2x2ID
*
* @return ID
*/
@Override
protected int getLayoutId() {
@ -55,11 +58,12 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
* 2XID
* ID
* UI
* @param bgId IDNotes
* @return int 2XDrawableID
* ID
* <p>
* ID2x2ID
*
* @param bgId ID
* @return ID
*/
@Override
protected int getBgResourceId(int bgId) {
@ -67,13 +71,14 @@ public class NoteWidgetProvider_2x extends NoteWidgetProvider {
}
/**
*
* NotesProvider便
* 便-
* @return int 2XNotes.TYPE_WIDGET_2X
*
* <p>
* 2x2
*
* @return
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_2X;
}
}
}

@ -14,35 +14,31 @@
* limitations under the License.
*/
// 包声明:小米便签 桌面小部件功能模块,该包统一承载所有尺寸规格的便签桌面小组件实现类
package net.micode.notes.widget;
// 安卓系统核心依赖:桌面小部件全生命周期调度的核心管理类,负责小部件的创建、更新、销毁等系统操作
import android.appwidget.AppWidgetManager;
// 安卓系统核心依赖:应用全局上下文,提供资源访问、系统服务调用、组件通信的基础能力,小部件初始化必备
import android.content.Context;
// 小米便签业务依赖应用资源常量类统一管理所有布局、样式等资源ID引用
import net.micode.notes.R;
// 小米便签业务依赖:数据层核心常量类,定义便签及小部件的类型、状态等全局业务枚举常量
import net.micode.notes.data.Notes;
// 小米便签业务依赖:资源解析工具类,封装多规格小部件背景资源的映射适配逻辑,统一提供背景资源获取能力
import net.micode.notes.tool.ResourceParser;
/**
* 便 4X
* NoteWidgetProviderMVCUI4X
* 4X
*
* 4x4 便
* <p>
* NoteWidgetProvider4x4便
*
*/
public class NoteWidgetProvider_4x extends NoteWidgetProvider {
/**
*
* 4X便
* 4Xupdate4X便
* @param context 访
* @param appWidgetManager
* @param appWidgetIds 4XID
*
* <p>
* update
*
* @param context
* @param appWidgetManager
* @param appWidgetIds ID
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
@ -50,20 +46,24 @@ public class NoteWidgetProvider_4x extends NoteWidgetProvider {
}
/**
* 4XID
* 便4XUI4便
* @return int 4XIDR.layout.widget_4x
* ID
* <p>
* 4x4ID
*
* @return ID
*/
@Override
protected int getLayoutId() {
return R.layout.widget_4x;
}
/**
* ID4XID
* UI
* 便
* @param bgId ID
* @return int 4XDrawableID
* ID
* <p>
* ID4x4ID
*
* @param bgId ID
* @return ID
*/
@Override
protected int getBgResourceId(int bgId) {
@ -71,13 +71,14 @@ public class NoteWidgetProvider_4x extends NoteWidgetProvider {
}
/**
* 4X
* NotesProvider便
* -
* @return int 4XNotes.TYPE_WIDGET_4X
*
* <p>
* 4x4
*
* @return
*/
@Override
protected int getWidgetType() {
return Notes.TYPE_WIDGET_4X;
}
}
}

Loading…
Cancel
Save