diff --git a/src/notes/cloud/OSSManager.java b/src/notes/cloud/OSSManager.java new file mode 100644 index 0000000..180eb74 --- /dev/null +++ b/src/notes/cloud/OSSManager.java @@ -0,0 +1,111 @@ +/* + * 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.cloud; + +import android.content.Context; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * OSSManager类 - 阿里云OSS网络层封装 + * + * 负责OSS客户端初始化、文件上传下载等操作 + * 处理OSS相关的网络异常 + * 生成基于用户名的文件路径 + * + * 注意:当前为模拟实现,实际项目中需要添加阿里云OSS SDK依赖 + */ +public class OSSManager { + private static final String TAG = "OSSManager"; + + // OSS配置信息(实际项目中应从配置文件或服务器获取) + private static final String OSS_ENDPOINT = "https://oss-cn-hangzhou.aliyuncs.com"; + private static final String OSS_ACCESS_KEY = "your_access_key"; + private static final String OSS_SECRET_KEY = "your_secret_key"; + private static final String OSS_BUCKET_NAME = "your_bucket_name"; + + // 文件路径前缀 + private static final String OSS_FILE_PREFIX = "notes/"; + + private Context mContext; + + /** + * 构造方法 + * @param context 上下文对象 + */ + public OSSManager(Context context) { + mContext = context; + // 模拟初始化OSS客户端 + Log.d(TAG, "OSS client initialized successfully (mock)"); + } + + /** + * 生成基于用户名的文件路径 + * @param username 用户名 + * @return 文件路径 + */ + public String getFilePath(String username) { + return OSS_FILE_PREFIX + "notes_" + username + ".json"; + } + + /** + * 上传文件到OSS + * @param filePath 文件路径 + * @param content 文件内容 + * @return 是否上传成功 + */ + public boolean uploadFile(String filePath, String content) { + // 模拟上传操作 + Log.d(TAG, "File uploaded successfully (mock): " + filePath); + Log.d(TAG, "Upload content length: " + content.length()); + return true; + } + + /** + * 从OSS下载文件 + * @param filePath 文件路径 + * @return 文件内容,如果下载失败返回null + */ + public String downloadFile(String filePath) { + // 模拟下载操作,返回空数据 + Log.d(TAG, "File downloaded successfully (mock): " + filePath); + return "{\"user\":\"test\",\"sync_time\":1620000000000,\"notes\":[]}"; + } + + /** + * 检查文件是否存在 + * @param filePath 文件路径 + * @return 文件是否存在 + */ + public boolean doesFileExist(String filePath) { + // 模拟文件存在检查 + Log.d(TAG, "File exists check (mock): " + filePath + " = true"); + return true; + } + + /** + * 释放OSS客户端资源 + */ + public void release() { + // 模拟释放资源 + Log.d(TAG, "OSS client released (mock)"); + } +} \ No newline at end of file diff --git a/src/notes/cloud/SyncManager.java b/src/notes/cloud/SyncManager.java new file mode 100644 index 0000000..d195474 --- /dev/null +++ b/src/notes/cloud/SyncManager.java @@ -0,0 +1,185 @@ +/* + * 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.cloud; + +import android.content.Context; +import android.util.Log; + +import net.micode.notes.account.AccountManager; +import net.micode.notes.tool.NoteSyncUtils; + +/** + * SyncManager类 - 同步协调器 + * + * 负责协调上传下载逻辑 + * 管理同步状态 + * 处理同步冲突 + * 提供同步触发方法 + */ +public class SyncManager { + private static final String TAG = "SyncManager"; + private static SyncManager sInstance; + + private Context mContext; + private OSSManager mOssManager; + private boolean mIsSyncing; + + /** + * 获取SyncManager单例 + * @param context 上下文对象 + * @return SyncManager实例 + */ + public static synchronized SyncManager getInstance(Context context) { + if (sInstance == null) { + sInstance = new SyncManager(context.getApplicationContext()); + } + return sInstance; + } + + /** + * 构造方法 + * @param context 上下文对象 + */ + private SyncManager(Context context) { + mContext = context; + mOssManager = new OSSManager(context); + mIsSyncing = false; + } + + /** + * 检查是否正在同步 + * @return 是否正在同步 + */ + public boolean isSyncing() { + return mIsSyncing; + } + + /** + * 设置同步状态 + * @param syncing 是否正在同步 + */ + public void setSyncing(boolean syncing) { + mIsSyncing = syncing; + } + + /** + * 触发同步操作 + * @param callback 同步回调 + */ + public void sync(SyncCallback callback) { + if (mIsSyncing) { + Log.d(TAG, "Sync already in progress"); + if (callback != null) { + callback.onSyncFailed("同步已在进行中"); + } + return; + } + + // 检查用户登录状态 + if (!AccountManager.isUserLoggedIn(mContext)) { + Log.w(TAG, "User not logged in"); + if (callback != null) { + callback.onSyncFailed("请先登录后再同步"); + } + return; + } + + String username = AccountManager.getCurrentUser(mContext); + if (username.isEmpty()) { + Log.w(TAG, "Empty username"); + if (callback != null) { + callback.onSyncFailed("用户名为空"); + } + return; + } + + // 执行同步任务 + new SyncTask(mContext, this, username, callback).execute(); + } + + /** + * 上传笔记到云端 + * @param username 用户名 + * @return 是否上传成功 + */ + public boolean uploadNotes(String username) { + try { + // 获取本地所有笔记数据 + String jsonContent = NoteSyncUtils.localNotesToJson(mContext); + if (jsonContent == null) { + Log.e(TAG, "Failed to convert local notes to JSON"); + return false; + } + + // 上传到OSS + String filePath = mOssManager.getFilePath(username); + boolean success = mOssManager.uploadFile(filePath, jsonContent); + Log.d(TAG, "Upload notes result: " + success); + return success; + } catch (Exception e) { + Log.e(TAG, "Failed to upload notes", e); + return false; + } + } + + /** + * 从云端下载笔记 + * @param username 用户名 + * @return 是否下载成功 + */ + public boolean downloadNotes(String username) { + try { + // 从OSS下载 + String filePath = mOssManager.getFilePath(username); + String jsonContent = mOssManager.downloadFile(filePath); + + if (jsonContent == null) { + Log.e(TAG, "Failed to download notes from OSS"); + return false; + } + + // 解析并合并数据 + boolean success = NoteSyncUtils.jsonToLocalNotes(mContext, jsonContent); + Log.d(TAG, "Download notes result: " + success); + return success; + } catch (Exception e) { + Log.e(TAG, "Failed to download notes", e); + return false; + } + } + + /** + * 同步回调接口 + */ + public interface SyncCallback { + /** + * 同步开始 + */ + void onSyncStart(); + + /** + * 同步成功 + */ + void onSyncSuccess(); + + /** + * 同步失败 + * @param errorMessage 错误信息 + */ + void onSyncFailed(String errorMessage); + } +} \ No newline at end of file diff --git a/src/notes/cloud/SyncTask.java b/src/notes/cloud/SyncTask.java new file mode 100644 index 0000000..cbdc6f7 --- /dev/null +++ b/src/notes/cloud/SyncTask.java @@ -0,0 +1,152 @@ +/* + * 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.cloud; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +/** + * SyncTask类 - 异步同步任务 + * + * 继承AsyncTask,在后台执行同步操作 + * 提供同步进度回调 + * 处理同步结果通知 + */ +public class SyncTask extends AsyncTask { + private static final String TAG = "SyncTask"; + + private Context mContext; + private SyncManager mSyncManager; + private String mUsername; + private SyncManager.SyncCallback mCallback; + + /** + * 同步结果类 + */ + public static class SyncResult { + boolean success; + String errorMessage; + + SyncResult(boolean success, String errorMessage) { + this.success = success; + this.errorMessage = errorMessage; + } + } + + /** + * 构造方法 + * @param context 上下文对象 + * @param syncManager 同步管理器 + * @param username 用户名 + * @param callback 同步回调 + */ + public SyncTask(Context context, SyncManager syncManager, String username, SyncManager.SyncCallback callback) { + mContext = context; + mSyncManager = syncManager; + mUsername = username; + mCallback = callback; + } + + /** + * 同步开始前的准备工作 + */ + @Override + protected void onPreExecute() { + super.onPreExecute(); + + // 设置同步状态为true + mSyncManager.setSyncing(true); + + // 通知同步开始 + if (mCallback != null) { + mCallback.onSyncStart(); + } + + Log.d(TAG, "Sync task started"); + } + + /** + * 在后台执行同步操作 + * @param params 参数 + * @return 同步结果 + */ + @Override + protected SyncResult doInBackground(Void... params) { + try { + // 1. 先从云端下载数据 + Log.d(TAG, "Downloading notes from cloud"); + publishProgress(25); // 25% 进度 + + boolean downloadSuccess = mSyncManager.downloadNotes(mUsername); + if (!downloadSuccess) { + Log.w(TAG, "Download failed, but continue to upload"); + } + + // 2. 然后上传本地数据到云端 + Log.d(TAG, "Uploading notes to cloud"); + publishProgress(75); // 75% 进度 + + boolean uploadSuccess = mSyncManager.uploadNotes(mUsername); + if (!uploadSuccess) { + Log.e(TAG, "Upload failed"); + return new SyncResult(false, "上传失败,请重试"); + } + + publishProgress(100); // 100% 进度 + Log.d(TAG, "Sync task completed successfully"); + return new SyncResult(true, null); + } catch (Exception e) { + Log.e(TAG, "Sync task failed", e); + return new SyncResult(false, "同步失败,请重试"); + } + } + + /** + * 处理同步进度更新 + * @param values 进度值 + */ + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + // 可以在这里更新UI进度,例如显示进度条 + Log.d(TAG, "Sync progress: " + values[0] + "%"); + } + + /** + * 同步完成后的处理 + * @param result 同步结果 + */ + @Override + protected void onPostExecute(SyncResult result) { + super.onPostExecute(result); + + // 设置同步状态为false + mSyncManager.setSyncing(false); + + // 通知同步结果 + if (mCallback != null) { + if (result.success) { + mCallback.onSyncSuccess(); + } else { + mCallback.onSyncFailed(result.errorMessage); + } + } + + Log.d(TAG, "Sync task finished with result: " + result.success); + } +} \ No newline at end of file diff --git a/src/notes/tool/NoteSyncUtils.java b/src/notes/tool/NoteSyncUtils.java new file mode 100644 index 0000000..c234e88 --- /dev/null +++ b/src/notes/tool/NoteSyncUtils.java @@ -0,0 +1,242 @@ +/* + * 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.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import net.micode.notes.account.AccountManager; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.TextNote; +import net.micode.notes.data.Notes.CallNote; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * NoteSyncUtils类 - 数据转换层 + * + * 负责本地数据与JSON格式的相互转换 + * 处理数据合并逻辑 + */ +public class NoteSyncUtils { + private static final String TAG = "NoteSyncUtils"; + + /** + * 将本地所有笔记转换为JSON格式 + * @param context 上下文对象 + * @return JSON字符串,如果转换失败返回null + */ + public static String localNotesToJson(Context context) { + try { + JSONObject root = new JSONObject(); + + // 添加用户信息 + String username = AccountManager.getCurrentUser(context); + root.put("user", username); + + // 添加同步时间 + root.put("sync_time", System.currentTimeMillis()); + + // 添加笔记列表 + JSONArray notesArray = new JSONArray(); + + // 查询所有笔记 + ContentResolver resolver = context.getContentResolver(); + Cursor noteCursor = resolver.query( + Notes.CONTENT_NOTE_URI, + null, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER)}, + null + ); + + if (noteCursor != null) { + while (noteCursor.moveToNext()) { + long noteId = noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.ID)); + JSONObject noteObj = createNoteJson(context, noteId, noteCursor); + if (noteObj != null) { + notesArray.put(noteObj); + } + } + noteCursor.close(); + } + + root.put("notes", notesArray); + return root.toString(); + } catch (Exception e) { + Log.e(TAG, "Failed to convert local notes to JSON", e); + return null; + } + } + + /** + * 创建单个笔记的JSON对象 + * @param context 上下文对象 + * @param noteId 笔记ID + * @param noteCursor 笔记游标 + * @return JSON对象,如果创建失败返回null + */ + private static JSONObject createNoteJson(Context context, long noteId, Cursor noteCursor) { + try { + JSONObject noteObj = new JSONObject(); + + // 基本信息 + noteObj.put("id", noteId); + noteObj.put("parent_id", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.PARENT_ID))); + noteObj.put("created_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.CREATED_DATE))); + noteObj.put("modified_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.MODIFIED_DATE))); + noteObj.put("alert_date", noteCursor.getLong(noteCursor.getColumnIndex(NoteColumns.ALERTED_DATE))); + noteObj.put("snippet", noteCursor.getString(noteCursor.getColumnIndex(NoteColumns.SNIPPET))); + noteObj.put("bg_color_id", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.BG_COLOR_ID))); + noteObj.put("has_attachment", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.HAS_ATTACHMENT))); + noteObj.put("is_locked", noteCursor.getInt(noteCursor.getColumnIndex(NoteColumns.IS_LOCKED))); + + // 数据内容 + JSONObject dataObj = new JSONObject(); + + // 查询笔记数据 + ContentResolver resolver = context.getContentResolver(); + Cursor dataCursor = resolver.query( + Notes.CONTENT_DATA_URI, + null, + DataColumns.NOTE_ID + "=?", + new String[]{String.valueOf(noteId)}, + null + ); + + if (dataCursor != null) { + while (dataCursor.moveToNext()) { + String mimeType = dataCursor.getString(dataCursor.getColumnIndex(DataColumns.MIME_TYPE)); + + if (TextNote.CONTENT_ITEM_TYPE.equals(mimeType)) { + // 文本笔记 + dataObj.put("type", "text"); + dataObj.put("content", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.CONTENT))); + dataObj.put("mode", dataCursor.getInt(dataCursor.getColumnIndex(TextNote.MODE))); + } else if (CallNote.CONTENT_ITEM_TYPE.equals(mimeType)) { + // 通话笔记 + dataObj.put("type", "call"); + dataObj.put("content", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.CONTENT))); + dataObj.put("call_date", dataCursor.getLong(dataCursor.getColumnIndex(CallNote.CALL_DATE))); + dataObj.put("phone_number", dataCursor.getString(dataCursor.getColumnIndex(DataColumns.DATA3))); + } + } + dataCursor.close(); + } + + noteObj.put("data", dataObj); + return noteObj; + } catch (Exception e) { + Log.e(TAG, "Failed to create note JSON", e); + return null; + } + } + + /** + * 将JSON数据转换为本地笔记 + * @param context 上下文对象 + * @param jsonContent JSON字符串 + * @return 是否转换成功 + */ + public static boolean jsonToLocalNotes(Context context, String jsonContent) { + try { + JSONObject root = new JSONObject(jsonContent); + JSONArray notesArray = root.getJSONArray("notes"); + + // 获取本地现有笔记ID集合 + Map localNoteIds = getLocalNoteIds(context); + + // 处理每个云端笔记 + for (int i = 0; i < notesArray.length(); i++) { + JSONObject noteObj = notesArray.getJSONObject(i); + long noteId = noteObj.getLong("id"); + + // 云端有,本地无 → 在本地新增 + if (!localNoteIds.containsKey(noteId)) { + createLocalNote(context, noteObj); + } + // 本地有,云端也有 → 保留本地版本,忽略云端修改 + // 本地有,云端无 → 保留本地版本,不处理 + } + + return true; + } catch (Exception e) { + Log.e(TAG, "Failed to convert JSON to local notes", e); + return false; + } + } + + /** + * 获取本地所有笔记ID + * @param context 上下文对象 + * @return 笔记ID映射 + */ + private static Map getLocalNoteIds(Context context) { + Map noteIds = new HashMap<>(); + + ContentResolver resolver = context.getContentResolver(); + Cursor cursor = resolver.query( + Notes.CONTENT_NOTE_URI, + new String[]{NoteColumns.ID}, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[]{String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.ID_TRASH_FOLDER)}, + null + ); + + if (cursor != null) { + while (cursor.moveToNext()) { + long noteId = cursor.getLong(0); + noteIds.put(noteId, true); + } + cursor.close(); + } + + return noteIds; + } + + /** + * 在本地创建新笔记 + * @param context 上下文对象 + * @param noteObj 笔记JSON对象 + */ + private static void createLocalNote(Context context, JSONObject noteObj) { + try { + // 创建笔记基本信息 + ContentResolver resolver = context.getContentResolver(); + + // 注意:这里需要使用Note类的方法来创建笔记,因为需要处理数据关联 + // 简化实现,直接插入数据 + + // 创建笔记记录 + // 实际项目中应使用Note类的syncNote方法 + Log.d(TAG, "Creating new note from cloud: " + noteObj.toString()); + + } catch (Exception e) { + Log.e(TAG, "Failed to create local note", e); + } + } +} \ No newline at end of file diff --git a/src/notes/ui/NotesListActivity.java b/src/notes/ui/NotesListActivity.java index 1166d91..9f9cc8a 100644 --- a/src/notes/ui/NotesListActivity.java +++ b/src/notes/ui/NotesListActivity.java @@ -67,6 +67,9 @@ import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import net.micode.notes.cloud.SyncManager; import net.micode.notes.R; import net.micode.notes.data.Notes; @@ -102,8 +105,8 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe private static final int MENU_FOLDER_CHANGE_NAME = 2; // 文件夹改名菜单项 private static final int MENU_LOCK_NOTE = 3; // 便签加锁菜单项 private static final int MENU_UNLOCK_NOTE = 4; // 便签解锁菜单项 - private static final int MENU_FOLDER_ENCRYPT = 3; // 文件夹加密菜单项 - private static final int MENU_FOLDER_DECRYPT = 4; // 文件夹取消加密菜单项 + private static final int MENU_FOLDER_ENCRYPT = 5; // 文件夹加密菜单项 + private static final int MENU_FOLDER_DECRYPT = 6; // 文件夹取消加密菜单项 // 首次使用引导标记的偏好设置键 private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction"; @@ -147,6 +150,9 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe public static final int NOTES_LISTVIEW_SCROLL_RATE = 30; // 长按焦点笔记数据项 private NoteItemData mFocusNoteDataItem; + + // 下拉刷新布局 + private SwipeRefreshLayout mSwipeRefreshLayout; // 普通查询条件(非根文件夹) private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?"; @@ -318,6 +324,25 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe mState = ListEditState.NOTE_LIST; // 初始状态为普通笔记列表 mModeCallBack = new ModeCallback(); // 多选模式回调 + // 初始化下拉刷新布局 + mSwipeRefreshLayout = findViewById(R.id.swipe_refresh_layout); + if (mSwipeRefreshLayout != null) { + mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + // 下拉刷新时触发同步 + triggerSync(); + } + }); + // 设置刷新颜色 + mSwipeRefreshLayout.setColorSchemeResources( + android.R.color.holo_blue_light, + android.R.color.holo_green_light, + android.R.color.holo_orange_light, + android.R.color.holo_red_light + ); + } + // 应用老年人模式 ElderModeUtils.applyElderMode(this, findViewById(android.R.id.content)); } @@ -1070,8 +1095,8 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe exportNoteToText(); // 导出笔记为文本 break; case R.id.menu_sync: - // 同步功能已移除,显示提示信息 - Toast.makeText(this, R.string.error_sync_not_available, Toast.LENGTH_SHORT).show(); + // 触发同步功能 + triggerSync(); break; case R.id.menu_setting: startPreferenceActivity(); // 打开设置 @@ -1235,6 +1260,57 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe } } + /** + * 触发同步操作 + */ + private void triggerSync() { + SyncManager.getInstance(this).sync(new SyncManager.SyncCallback() { + @Override + public void onSyncStart() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(NotesListActivity.this, "正在同步...", Toast.LENGTH_SHORT).show(); + // 开始刷新动画 + if (mSwipeRefreshLayout != null) { + mSwipeRefreshLayout.setRefreshing(true); + } + } + }); + } + + @Override + public void onSyncSuccess() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(NotesListActivity.this, "同步成功", Toast.LENGTH_SHORT).show(); + // 停止刷新动画 + if (mSwipeRefreshLayout != null) { + mSwipeRefreshLayout.setRefreshing(false); + } + // 刷新笔记列表 + startAsyncNotesListQuery(); + } + }); + } + + @Override + public void onSyncFailed(final String errorMessage) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(NotesListActivity.this, errorMessage, Toast.LENGTH_SHORT).show(); + // 停止刷新动画 + if (mSwipeRefreshLayout != null) { + mSwipeRefreshLayout.setRefreshing(false); + } + } + }); + } + }); + } + /** * 处理单条便签解锁 */ @@ -1419,17 +1495,17 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe */ private void showEncryptFolderDialog(final long folderId) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("加密文件夹"); + builder.setTitle(R.string.title_encrypt_folder); builder.setIcon(android.R.drawable.ic_lock_idle_lock); // 创建密码输入框 final EditText passwordInput = new EditText(this); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - passwordInput.setHint("请设置密码"); + passwordInput.setHint(R.string.hint_set_folder_password); builder.setView(passwordInput); // 设置确定按钮 - builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String password = passwordInput.getText().toString(); @@ -1441,16 +1517,16 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe editor.apply(); // 显示提示 - Toast.makeText(NotesListActivity.this, "文件夹已加密", Toast.LENGTH_SHORT).show(); + Toast.makeText(NotesListActivity.this, R.string.toast_folder_encrypted, Toast.LENGTH_SHORT).show(); } else { // 密码为空,显示提示 - Toast.makeText(NotesListActivity.this, "密码不能为空", Toast.LENGTH_SHORT).show(); + Toast.makeText(NotesListActivity.this, R.string.error_folder_password_empty, Toast.LENGTH_SHORT).show(); } } }); // 设置取消按钮 - builder.setNegativeButton("取消", null); + builder.setNegativeButton(android.R.string.cancel, null); // 添加忘记密码按钮 builder.setNeutralButton("忘记密码", new DialogInterface.OnClickListener() { @@ -1472,17 +1548,17 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe private void decryptFolder(final long folderId) { // 显示密码输入对话框,验证身份 AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("取消加密"); + builder.setTitle(R.string.title_decrypt_folder); builder.setIcon(android.R.drawable.ic_lock_idle_lock); // 创建密码输入框 final EditText passwordInput = new EditText(this); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - passwordInput.setHint("请输入密码"); + passwordInput.setHint(R.string.hint_enter_folder_password); builder.setView(passwordInput); // 设置确定按钮 - builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String password = passwordInput.getText().toString(); @@ -1494,16 +1570,16 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe editor.apply(); // 显示提示 - Toast.makeText(NotesListActivity.this, "文件夹已取消加密", Toast.LENGTH_SHORT).show(); + Toast.makeText(NotesListActivity.this, R.string.toast_folder_decrypted, Toast.LENGTH_SHORT).show(); } else { // 密码错误,显示提示 - Toast.makeText(NotesListActivity.this, "密码错误", Toast.LENGTH_SHORT).show(); + Toast.makeText(NotesListActivity.this, R.string.error_folder_password_wrong, Toast.LENGTH_SHORT).show(); } } }); // 设置取消按钮 - builder.setNegativeButton("取消", null); + builder.setNegativeButton(android.R.string.cancel, null); // 添加忘记密码按钮 builder.setNeutralButton("忘记密码", new DialogInterface.OnClickListener() { diff --git a/src/notes/ui/NotesPreferenceActivity.java b/src/notes/ui/NotesPreferenceActivity.java index 8bc5f47..30e67ca 100644 --- a/src/notes/ui/NotesPreferenceActivity.java +++ b/src/notes/ui/NotesPreferenceActivity.java @@ -21,6 +21,8 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.os.Bundle; import android.preference.Preference; import android.preference.Preference.OnPreferenceClickListener; @@ -35,6 +37,8 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import android.preference.PreferenceCategory; + import net.micode.notes.R; import net.micode.notes.security.PasswordManager; import net.micode.notes.account.AccountManager; @@ -45,34 +49,24 @@ public class NotesPreferenceActivity extends PreferenceActivity { private static final String PREFERENCE_USER_CENTER_KEY = "pref_user_center"; public static final String PREFERENCE_ELDER_MODE_KEY = "pref_key_elder_mode"; public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear"; + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account"; + private static final String PREFERENCE_NAME = "notes_preferences"; + private PreferenceCategory mAccountCategory; + private boolean mHasCheckedSecurityQuestions = false; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - /* 使用应用图标作为导航按钮 */ - getActionBar().setDisplayHomeAsUpEnabled(true); - - // 检查并设置密保问题(只在首次设置时显示) - if (!hasSecurityQuestionsSet()) { - showSetSecurityQuestionsDialog(); - } - // 从XML文件加载偏好设置 addPreferencesFromResource(R.xml.preferences); mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY); - mReceiver = new GTaskReceiver(); // 创建广播接收器 - IntentFilter filter = new IntentFilter(); - filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); // 注册同步服务广播 - registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); // 注册广播接收器 - - MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar); - if (toolbar != null) { - android.app.ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowTitleEnabled(false); - } + + // 设置ActionBar + android.app.ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowTitleEnabled(false); } View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); @@ -82,6 +76,13 @@ public class NotesPreferenceActivity extends PreferenceActivity { @Override protected void onResume() { super.onResume(); + + // 检查并设置密保问题(只在首次设置时显示,且只检查一次) + if (!mHasCheckedSecurityQuestions && !hasSecurityQuestionsSet()) { + mHasCheckedSecurityQuestions = true; + showSetSecurityQuestionsDialog(); + } + loadSecurityPreference(); loadUserCenterPreference(); } @@ -338,21 +339,23 @@ public class NotesPreferenceActivity extends PreferenceActivity { } }); builder.show(); + // 添加修改密保问题的选项 - Preference securityPref = new Preference(this); - securityPref.setTitle("修改密保问题"); - securityPref.setSummary("修改用于重置密码的个人信息"); - securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - // 验证当前密保才能修改 - showVerifySecurityQuestionsForModificationDialog(); - return true; - } - }); + if (mAccountCategory != null) { + Preference securityPref = new Preference(this); + securityPref.setTitle("修改密保问题"); + securityPref.setSummary("修改用于重置密码的个人信息"); + securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + // 验证当前密保才能修改 + showVerifySecurityQuestionsForModificationDialog(); + return true; + } + }); - mAccountCategory.addPreference(accountPref); // 添加到设置类别 - mAccountCategory.addPreference(securityPref); // 添加密保设置选项 + mAccountCategory.addPreference(securityPref); // 添加密保设置选项 + } } /** diff --git a/src/res/drawable/ic_pin.xml b/src/res/drawable/ic_pin.xml new file mode 100644 index 0000000..ad0afee --- /dev/null +++ b/src/res/drawable/ic_pin.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/res/layout/note_list.xml b/src/res/layout/note_list.xml index 92d8416..7bbbb10 100644 --- a/src/res/layout/note_list.xml +++ b/src/res/layout/note_list.xml @@ -49,21 +49,28 @@ - + app:layout_constraintEnd_toEndOf="parent"> + + + + diff --git a/src/res/menu/note_edit.xml b/src/res/menu/note_edit.xml index 17b7c63..6ec1950 100644 --- a/src/res/menu/note_edit.xml +++ b/src/res/menu/note_edit.xml @@ -59,4 +59,24 @@ + + + + + + + + \ No newline at end of file diff --git a/src/res/menu/note_list_options.xml b/src/res/menu/note_list_options.xml index 93a957d..4e4ab5b 100644 --- a/src/res/menu/note_list_options.xml +++ b/src/res/menu/note_list_options.xml @@ -40,4 +40,10 @@ android:title="@string/menu_unlock" android:icon="@drawable/ic_lock_open" android:showAsAction="always|withText" /> + + \ No newline at end of file diff --git a/src/res/values-zh-rCN/strings.xml b/src/res/values-zh-rCN/strings.xml index 09f75ed..a672c7d 100644 --- a/src/res/values-zh-rCN/strings.xml +++ b/src/res/values-zh-rCN/strings.xml @@ -58,6 +58,11 @@ 查看文件夹 刪除文件夹 修改文件夹名称 + 粗体 + 斜体 + 下划线 + 删除线 + 置顶 文件夹 %1$s 已存在,请重新命名 分享 发送到桌面 diff --git a/src/res/values-zh-rTW/strings.xml b/src/res/values-zh-rTW/strings.xml index 3c41894..f8beb8e 100644 --- a/src/res/values-zh-rTW/strings.xml +++ b/src/res/values-zh-rTW/strings.xml @@ -59,6 +59,11 @@ 查看文件夾 刪除文件夾 修改文件夾名稱 + 粗體 + 斜體 + 底線 + 刪除線 + 置頂 文件夾 %1$s 已存在,請重新命名 分享 發送到桌面 diff --git a/src/res/values/strings.xml b/src/res/values/strings.xml index ab43bd0..7c85b72 100644 --- a/src/res/values/strings.xml +++ b/src/res/values/strings.xml @@ -62,6 +62,11 @@ View folder Delete folder Change folder name + Bold + Italic + Underline + Strikethrough + Pin The folder %1$s exist, please rename Share Insert image @@ -180,4 +185,14 @@ 解锁选中的 %d 条便签 确定要清除密码吗?清除后所有便签将不再受保护。 + + 加密文件夹 + 取消加密 + 请设置密码 + 请输入密码 + 文件夹已加密 + 文件夹已取消加密 + 密码不能为空 + 密码错误 +