新增智能日程功能、底部导航栏、界面架构优化 #29

Merged
pjao9fvxr merged 2 commits from zhouzexin_branch into master 4 weeks ago

@ -8,7 +8,6 @@
android:id="@+id/note_list_root"
android:background="@drawable/list_background">
<!-- [Fix] 将根布局改为 RelativeLayout 以便精确控制高度 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -57,37 +56,62 @@
android:gravity="center_vertical" />
</HorizontalScrollView>
<!-- 4. 下拉刷新与列表容器 -->
<!-- [Fix] layout_below 指定在导航栏下方alignParentBottom 指定拉伸到底部 -->
<!-- 4. [核心新增] 底部导航栏 -->
<!-- 背景设为灰色,图标和文字设为黑色 -->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_alignParentBottom="true"
android:background="#F5F5F5"
app:itemIconTint="@android:color/black"
app:itemTextColor="@android:color/black"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
<!-- 5. 下拉刷新与列表容器 -->
<!-- 手术关键点:除了 layout_below必须增加 layout_above="@id/bottom_navigation" -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/folder_nav_scroll"
android:layout_alignParentBottom="true">
android:layout_above="@id/bottom_navigation">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/notes_list_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="80dp" />
android:paddingBottom="16dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 6. 用于承载“日程”和“AI”界面的占位容器 (默认隐藏) -->
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:layout_above="@id/bottom_navigation"
android:visibility="gone" />
</RelativeLayout>
<!-- 底部悬浮按钮 (FAB) 保持不变 -->
<!-- 7. 底部悬浮按钮 (FAB) -->
<!-- 调整 margin_bottom 以免挡住底部导航栏 -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_add_note"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|left"
android:layout_margin="24dp"
android:layout_marginLeft="24dp"
android:layout_marginBottom="80dp"
android:contentDescription="@string/notelist_menu_new"
android:src="@android:drawable/ic_input_add"
app:backgroundTint="@color/mi_gold"
app:fabSize="normal"
app:rippleColor="@color/pure_white"
app:elevation="6dp" />
</FrameLayout>

@ -24,4 +24,8 @@
<color name="mi_gold">#FFD700</color>
<color name="pure_white">#FFFFFF</color>
<color name="primary_text_black">#333333</color>
<color name="nav_background">#F5F5F5</color> <!-- 浅灰色背景 -->
<color name="nav_item_selected">#000000</color> <!-- 选中时为黑色 -->
<color name="nav_item_unselected">#757575</color> <!-- 未选中时为中灰色 -->
</resources>

@ -0,0 +1,33 @@
package net.micode.notes.model;
import com.google.firebase.firestore.IgnoreExtraProperties;
/**
* 便 - Firebase Firestore
*/
@IgnoreExtraProperties
public class CloudNote {
public String serverId; // 云端 ID
public String title; // 标题
public String content; // 完整正文内容
public long modifiedDate; // 修改时间
public int bgColorId; // 背景颜色 ID
public int isAgenda; // 是否为日程 (0/1)
public long agendaDate; // 日程时间戳
public int isCompleted; // 是否完成 (0/1)
// Firebase 必须要求的空构造函数
public CloudNote() {}
public CloudNote(String serverId, String title, String content, long modifiedDate,
int bgColorId, int isAgenda, long agendaDate, int isCompleted) {
this.serverId = serverId;
this.title = title;
this.content = content;
this.modifiedDate = modifiedDate;
this.bgColorId = bgColorId;
this.isAgenda = isAgenda;
this.agendaDate = agendaDate;
this.isCompleted = isCompleted;
}
}

@ -0,0 +1,206 @@
package net.micode.notes.sync;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.CloudNote;
import net.micode.notes.tool.SyncMapper;
import java.util.UUID;
public class SyncWorker extends Worker {
private static final String TAG = "SyncWorker";
public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
// [确切位置]:在方法入口处立即进行安全隔离
try {
String uid = FirebaseAuth.getInstance().getUid();
if (uid == null) {
Log.w(TAG, "Sync skipped: No user logged in.");
return Result.success();
}
FirebaseFirestore db = FirebaseFirestore.getInstance();
// 1. 先执行上行同步
performPush(db, uid);
// 2. 后执行下行同步
performPull(db, uid);
return Result.success();
} catch (SecurityException e) {
// [核心修复]:捕获 "Unknown calling package name 'com.google.android.gms'"
Log.e(TAG, "GMS Security Exception caught, retrying later: " + e.getMessage());
return Result.retry(); // 告诉系统稍后重试,不要直接闪退进程
} catch (Exception e) {
Log.e(TAG, "General sync error: " + e.getMessage());
return Result.failure();
}
}
/**
* [] sync_state = 1
*/
private void performPush(FirebaseFirestore db, String uid) {
Cursor cursor = getApplicationContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.SYNC_STATE + "=? AND " + NoteColumns.TYPE + "=? AND " + NoteColumns.ID + ">0",
new String[]{"1", String.valueOf(Notes.TYPE_NOTE)},
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
long localId = cursor.getLong(cursor.getColumnIndex(NoteColumns.ID));
String serverId = cursor.getString(cursor.getColumnIndex(NoteColumns.SERVER_ID));
// 如果没有 serverId说明是本地新建的分配一个 UUID
if (TextUtils.isEmpty(serverId)) {
serverId = UUID.randomUUID().toString();
}
// 获取完整正文 (跨表查询 data 表)
String fullContent = getNoteFullContent(localId);
// 翻译为云端对象
CloudNote cloudNote = SyncMapper.fromCursor(cursor, fullContent);
cloudNote.serverId = serverId;
// 上传 Firestore
final String finalServerId = serverId;
db.collection("users").document(uid)
.collection("notes").document(finalServerId)
.set(cloudNote)
.addOnSuccessListener(aVoid -> {
// 同步成功,回写本地状态为 0 (已同步)
ContentValues values = new ContentValues();
values.put(NoteColumns.SYNC_STATE, 0);
values.put(NoteColumns.SERVER_ID, finalServerId);
getApplicationContext().getContentResolver().update(
Uri.withAppendedPath(Notes.CONTENT_NOTE_URI, String.valueOf(localId)),
values, null, null);
Log.d(TAG, "Push Success: " + finalServerId);
});
}
cursor.close();
}
}
/**
* []
*/
private void performPull(FirebaseFirestore db, String uid) {
db.collection("users").document(uid).collection("notes")
.get()
.addOnSuccessListener(queryDocumentSnapshots -> {
for (QueryDocumentSnapshot doc : queryDocumentSnapshots) {
CloudNote cloudNote = doc.toObject(CloudNote.class);
if (cloudNote != null) {
mergeCloudNoteToLocal(cloudNote);
}
}
Log.d(TAG, "Pull complete. Cloud docs processed.");
})
.addOnFailureListener(e -> Log.e(TAG, "Pull failed", e));
}
/**
* []
*/
private void mergeCloudNoteToLocal(CloudNote cloudNote) {
Cursor c = getApplicationContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID, NoteColumns.MODIFIED_DATE, NoteColumns.SYNC_STATE},
NoteColumns.SERVER_ID + "=?",
new String[]{cloudNote.serverId},
null
);
if (c != null && c.moveToFirst()) {
// 本地已存在该便签
long localId = c.getLong(0);
long localModified = c.getLong(1);
int syncState = c.getInt(2);
// 只有当云端修改时间更新,且本地不是“待上传”状态时,才覆盖本地
if (cloudNote.modifiedDate > localModified && syncState == 0) {
updateLocalNote(localId, cloudNote);
}
c.close();
} else {
// 本地没有,直接新建
insertCloudNoteToLocal(cloudNote);
}
}
private void insertCloudNoteToLocal(CloudNote cloudNote) {
ContentValues values = SyncMapper.toNoteValues(cloudNote);
Uri noteUri = getApplicationContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
if (noteUri != null) {
long newId = ContentUris.parseId(noteUri);
// 写入正文到 data 表
ContentValues dataValues = new ContentValues();
dataValues.put(Notes.DataColumns.NOTE_ID, newId);
dataValues.put(Notes.DataColumns.MIME_TYPE, Notes.TextNote.CONTENT_ITEM_TYPE);
dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
getApplicationContext().getContentResolver().insert(Notes.CONTENT_DATA_URI, dataValues);
}
}
private void updateLocalNote(long localId, CloudNote cloudNote) {
ContentValues values = SyncMapper.toNoteValues(cloudNote);
getApplicationContext().getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, localId),
values, null, null);
ContentValues dataValues = new ContentValues();
dataValues.put(Notes.DataColumns.CONTENT, cloudNote.content);
getApplicationContext().getContentResolver().update(
Notes.CONTENT_DATA_URI,
dataValues,
Notes.DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(localId)});
}
private String getNoteFullContent(long noteId) {
Cursor c = getApplicationContext().getContentResolver().query(
Notes.CONTENT_DATA_URI,
new String[]{Notes.DataColumns.CONTENT},
Notes.DataColumns.NOTE_ID + "=? AND " + Notes.DataColumns.MIME_TYPE + "=?",
new String[]{String.valueOf(noteId), Notes.TextNote.CONTENT_ITEM_TYPE},
null
);
String content = "";
if (c != null) {
if (c.moveToFirst()) content = c.getString(0);
c.close();
}
return content;
}
}

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

@ -0,0 +1,127 @@
package net.micode.notes.ui;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CalendarView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import java.util.Calendar;
public class AgendaFragment extends Fragment {
private RecyclerView mRecyclerView;
private NotesListItemAdapter mAdapter;
private CalendarView mCalendarView;
private TextView mTvEmpty;
private long mSelectedDayStart;
private long mSelectedDayEnd;
private TextView tvDateHeader;
private View emptyState;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_agenda, container, false);
initUI(view);
return view;
}
private void initUI(View view) {
mCalendarView = view.findViewById(R.id.calendar_view);
mRecyclerView = view.findViewById(R.id.agenda_list);
tvDateHeader = view.findViewById(R.id.tv_agenda_date_header);
emptyState = view.findViewById(R.id.ll_empty_state);
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mAdapter = new NotesListItemAdapter(getContext());
mRecyclerView.setAdapter(mAdapter);
// 默认显示今天
updateDateDisplay(Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
mCalendarView.setOnDateChangeListener((view1, year, month, dayOfMonth) -> {
updateDateDisplay(year, month, dayOfMonth);
loadAgendaData();
});
loadAgendaData();
}
private void updateDateDisplay(int year, int month, int day) {
tvDateHeader.setText(String.format("%d年%d月%d日 的日程", year, month + 1, day));
Calendar cal = Calendar.getInstance();
cal.set(year, month, day);
calculateDayRange(cal.getTimeInMillis());
}
// 计算选中日期的 [00:00:00, 23:59:59] 时间戳
private void calculateDayRange(long timeInMillis) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(timeInMillis);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
mSelectedDayStart = cal.getTimeInMillis();
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
mSelectedDayEnd = cal.getTimeInMillis();
}
private void loadAgendaData() {
// 增加空检查,防止 fragment 已经 detach 时还执行异步回调
if (!isAdded() || getContext() == null) return;
ContentResolver resolver = getContext().getContentResolver();
// 使用异步查询(推荐)或者简单的后台线程
new Thread(() -> {
String selection = NoteColumns.IS_AGENDA + "=1 AND "
+ NoteColumns.AGENDA_DATE + " >= ? AND "
+ NoteColumns.AGENDA_DATE + " <= ?";
String[] selectionArgs = new String[] {
String.valueOf(mSelectedDayStart),
String.valueOf(mSelectedDayEnd)
};
// 执行查询
final Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI,
NoteItemData.PROJECTION,
selection,
selectionArgs,
NoteColumns.AGENDA_DATE + " ASC"
);
// 切回主线程更新 UI
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
mAdapter.changeCursor(cursor);
if (cursor == null || cursor.getCount() == 0) {
emptyState.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
emptyState.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
});
}
}).start();
}
}

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

@ -156,6 +156,8 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
setContentView(R.layout.note_list);
initResources();
setupBottomNavigation();
/**
* Insert an introduction when user firstly use this application
*/
@ -470,6 +472,55 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
});
}
private void setupBottomNavigation() {
com.google.android.material.bottomnavigation.BottomNavigationView navView = findViewById(R.id.bottom_navigation);
navView.setOnItemSelectedListener(item -> {
int itemId = item.getItemId();
// 1. 获取 Fragment 管理器
androidx.fragment.app.FragmentManager fm = getSupportFragmentManager();
androidx.fragment.app.FragmentTransaction ft = fm.beginTransaction();
// 2. 隐藏所有的交互组件(默认隐藏)
findViewById(R.id.swipe_refresh_layout).setVisibility(View.GONE);
findViewById(R.id.fragment_container).setVisibility(View.GONE);
mAddNewNote.hide();
if (itemId == R.id.nav_notes) {
// --- 显示便签列表模式 ---
findViewById(R.id.swipe_refresh_layout).setVisibility(View.VISIBLE);
mAddNewNote.show();
if (getSupportActionBar() != null) getSupportActionBar().setTitle(R.string.app_name);
// 移除可能存在的日程 Fragment释放资源
androidx.fragment.app.Fragment agenda = fm.findFragmentByTag("AGENDA");
if (agenda != null) ft.remove(agenda);
} else if (itemId == R.id.nav_agenda) {
// --- 切换到日程视图模式 ---
findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);
if (getSupportActionBar() != null) getSupportActionBar().setTitle("智能日程");
// 使用 TAG 查找,避免重复创建引发的 GMS 校验冲突
androidx.fragment.app.Fragment agenda = fm.findFragmentByTag("AGENDA");
if (agenda == null) {
// 确切位置:在此注入我们之前设计的 AgendaFragment
ft.replace(R.id.fragment_container, new AgendaFragment(), "AGENDA");
}
} else if (itemId == R.id.nav_ai) {
// --- AI 助手(预留) ---
findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);
if (getSupportActionBar() != null) getSupportActionBar().setTitle("AI 助理");
}
// 使用 commitAllowingStateLoss 彻底杜绝由于异步同步导致的 Activity 状态丢失闪退
ft.commitAllowingStateLoss();
return true;
});
}
private void showPasswordDialog() {
final EditText et = new EditText(this);
et.setInputType(android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD);

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