修复密码锁安全漏洞

pull/44/head
陶俊宇 4 weeks ago
parent 62df2add6e
commit fffe5ba193

@ -125,7 +125,7 @@
</activity>
<activity
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:name=".ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
@ -133,6 +133,13 @@
android:theme="@android:style/Theme.Holo.Light" >
</activity>
<activity
android:name=".ui.TemplateSelectActivity"
android:label="@string/menu_template"
android:theme="@style/NoteTheme"
android:launchMode="singleTop">
</activity>
<service
android:name="net.micode.notes.gtask.remote.GTaskSyncService"
android:exported="false" >

@ -265,6 +265,13 @@ public class Notes {
*/
public static final String LOCK_TYPE = "lock_type";
/**
*
* <P> : TEXT </P>
* <P> </P>
*/
public static final String LOCK_HINT = "lock_hint";
/**
* 便
* <P> : INTEGER (long) </P>

@ -35,7 +35,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 数据库文件名
private static final String DB_NAME = "note.db";
// 数据库版本号,用于升级控制
private static final int DB_VERSION = 7;
private static final int DB_VERSION = 8;
/**
*
@ -79,6 +79,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.IS_LOCKED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCK_PASSWORD + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.LOCK_TYPE + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.LOCK_HINT + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.DELETED_DATE + " INTEGER NOT NULL DEFAULT 0" +
")";
@ -425,6 +426,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
// 版本7升级到版本8添加密码提示字段
if (oldVersion == 7) {
upgradeToV8(db);
oldVersion++;
}
// 如果需要,重新创建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
@ -530,4 +537,14 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.ID + ";" +
" END");
}
/**
* 8
* @param db
*/
private void upgradeToV8(SQLiteDatabase db) {
// 添加LOCK_HINT字段用于存储密码提示
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.LOCK_HINT
+ " TEXT NOT NULL DEFAULT ''");
}
}

@ -0,0 +1,119 @@
package net.micode.notes.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class NoteTemplate {
public static final String TYPE_SYSTEM = "system";
public static final String TYPE_CUSTOM = "custom";
private long id;
private String name;
private String content;
private String type;
private long createTime;
private long updateTime;
public NoteTemplate() {
this.createTime = System.currentTimeMillis();
this.updateTime = System.currentTimeMillis();
}
public NoteTemplate(long id, String name, String content, String type, long createTime, long updateTime) {
this.id = id;
this.name = name;
this.content = content;
this.type = type;
this.createTime = createTime;
this.updateTime = updateTime;
}
// Getters and Setters
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public long getCreateTime() {
return createTime;
}
public void setCreateTime(long createTime) {
this.createTime = createTime;
}
public long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(long updateTime) {
this.updateTime = updateTime;
}
/**
* JSON
*/
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("id", id);
json.put("name", name);
json.put("content", content);
json.put("type", type);
json.put("createTime", createTime);
json.put("updateTime", updateTime);
return json;
}
/**
* JSON
*/
public static NoteTemplate fromJson(JSONObject json) throws JSONException {
NoteTemplate template = new NoteTemplate();
template.setId(json.getLong("id"));
template.setName(json.getString("name"));
template.setContent(json.getString("content"));
template.setType(json.getString("type"));
template.setCreateTime(json.getLong("createTime"));
template.setUpdateTime(json.getLong("updateTime"));
return template;
}
}

@ -0,0 +1,271 @@
package net.micode.notes.model;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class TemplateManager {
private static final String PREF_TEMPLATES = "note_templates";
private static final String PREF_TEMPLATE_ID_COUNTER = "template_id_counter";
private static TemplateManager sInstance;
private Context mContext;
private List<NoteTemplate> mTemplates;
private TemplateManager(Context context) {
mContext = context.getApplicationContext();
loadTemplates();
}
public static synchronized TemplateManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new TemplateManager(context);
}
return sInstance;
}
/**
*
*/
private void loadTemplates() {
mTemplates = new ArrayList<>();
// 先添加系统模板
addSystemTemplates();
// 然后加载自定义模板
loadCustomTemplates();
}
/**
*
*/
private void addSystemTemplates() {
// 会议记录模板
NoteTemplate meetingTemplate = new NoteTemplate();
meetingTemplate.setId(1);
meetingTemplate.setName("会议记录");
meetingTemplate.setContent("<strong>会议记录</strong><br><br>" +
"<strong>会议主题</strong><br>" +
"<strong>会议时间</strong><br>" +
"<strong>会议地点</strong><br>" +
"<strong>参会人员</strong><br>" +
"<strong>主持人</strong><br><br>" +
"<strong>会议议程</strong><br>" +
"1. <br>" +
"2. <br>" +
"3. <br><br>" +
"<strong>会议内容</strong><br><br>" +
"<strong>决议事项</strong><br>" +
"1. <br>" +
"2. <br>" +
"3. <br><br>" +
"<strong>行动项</strong><br>" +
"<strong>任务</strong><strong>负责人</strong><strong>截止日期</strong><strong>状态</strong><br><br>" +
"<br><br>" +
"<strong>下次会议</strong><br>" +
"<strong>时间</strong><br>" +
"<strong>主题</strong><br>");
meetingTemplate.setType(NoteTemplate.TYPE_SYSTEM);
mTemplates.add(meetingTemplate);
// 待办事项模板
NoteTemplate todoTemplate = new NoteTemplate();
todoTemplate.setId(2);
todoTemplate.setName("待办事项");
todoTemplate.setContent("<strong>待办事项</strong><br><br>" +
"<strong>日期</strong><br><br>" +
"<strong>今日待办</strong><br>" +
"☐ <br>" +
"☐ <br>" +
"☐ <br>" +
"☐ <br>" +
"☐ <br><br>" +
"<strong>重要事项</strong><br>" +
"⭐ <br>" +
"⭐ <br><br>" +
"<strong>已完成</strong><br>" +
"✓ <br>" +
"✓ <br>" +
"✓ <br>");
todoTemplate.setType(NoteTemplate.TYPE_SYSTEM);
mTemplates.add(todoTemplate);
// 购物清单模板
NoteTemplate shoppingTemplate = new NoteTemplate();
shoppingTemplate.setId(3);
shoppingTemplate.setName("购物清单");
shoppingTemplate.setContent("<strong>购物清单</strong><br><br>" +
"<strong>日期</strong><br>" +
"<strong>商店</strong><br>" +
"<strong>预算</strong><br><br>" +
"<strong>清单</strong><br>" +
"<strong>物品</strong><strong>数量</strong><strong>单价</strong><strong>备注</strong><strong>状态</strong><br>" +
" ☐ <br>" +
" ☐ <br>" +
" ☐ <br>" +
" ☐ <br>" +
" ☐ <br><br>" +
"<strong>总计</strong><br>" +
"<strong>实际花费</strong><br>" +
"<strong>节省/超支</strong><br>");
shoppingTemplate.setType(NoteTemplate.TYPE_SYSTEM);
mTemplates.add(shoppingTemplate);
}
/**
*
*/
private void loadCustomTemplates() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
String templatesJson = prefs.getString(PREF_TEMPLATES, "[]");
try {
JSONArray jsonArray = new JSONArray(templatesJson);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject json = jsonArray.getJSONObject(i);
NoteTemplate template = NoteTemplate.fromJson(json);
mTemplates.add(template);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* SharedPreferences
*/
private void saveCustomTemplates() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = prefs.edit();
JSONArray jsonArray = new JSONArray();
for (NoteTemplate template : mTemplates) {
if (NoteTemplate.TYPE_CUSTOM.equals(template.getType())) {
try {
jsonArray.put(template.toJson());
} catch (JSONException e) {
e.printStackTrace();
}
}
}
editor.putString(PREF_TEMPLATES, jsonArray.toString());
editor.apply();
}
/**
*
*/
public List<NoteTemplate> getAllTemplates() {
return new ArrayList<>(mTemplates);
}
/**
*
*/
public List<NoteTemplate> getSystemTemplates() {
List<NoteTemplate> systemTemplates = new ArrayList<>();
for (NoteTemplate template : mTemplates) {
if (NoteTemplate.TYPE_SYSTEM.equals(template.getType())) {
systemTemplates.add(template);
}
}
return systemTemplates;
}
/**
*
*/
public List<NoteTemplate> getCustomTemplates() {
List<NoteTemplate> customTemplates = new ArrayList<>();
for (NoteTemplate template : mTemplates) {
if (NoteTemplate.TYPE_CUSTOM.equals(template.getType())) {
customTemplates.add(template);
}
}
return customTemplates;
}
/**
* ID
*/
public NoteTemplate getTemplateById(long id) {
for (NoteTemplate template : mTemplates) {
if (template.getId() == id) {
return template;
}
}
return null;
}
/**
*
*/
public void addCustomTemplate(NoteTemplate template) {
// 生成唯一ID
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
long id = prefs.getLong(PREF_TEMPLATE_ID_COUNTER, 1000);
template.setId(id);
template.setType(NoteTemplate.TYPE_CUSTOM);
template.setCreateTime(System.currentTimeMillis());
template.setUpdateTime(System.currentTimeMillis());
mTemplates.add(template);
// 更新ID计数器
prefs.edit().putLong(PREF_TEMPLATE_ID_COUNTER, id + 1).apply();
// 保存自定义模板
saveCustomTemplates();
}
/**
*
*/
public boolean deleteCustomTemplate(long id) {
NoteTemplate templateToRemove = null;
for (NoteTemplate template : mTemplates) {
if (template.getId() == id && NoteTemplate.TYPE_CUSTOM.equals(template.getType())) {
templateToRemove = template;
break;
}
}
if (templateToRemove != null) {
mTemplates.remove(templateToRemove);
saveCustomTemplates();
return true;
}
return false;
}
/**
*
*/
public boolean updateCustomTemplate(NoteTemplate updatedTemplate) {
for (int i = 0; i < mTemplates.size(); i++) {
NoteTemplate template = mTemplates.get(i);
if (template.getId() == updatedTemplate.getId() &&
NoteTemplate.TYPE_CUSTOM.equals(template.getType())) {
updatedTemplate.setUpdateTime(System.currentTimeMillis());
mTemplates.set(i, updatedTemplate);
saveCustomTemplates();
return true;
}
}
return false;
}
}

@ -46,6 +46,9 @@ import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.util.TypedValue;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
@ -69,6 +72,7 @@ import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.TextAppearanceResources;
import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener;
import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener;
import net.micode.notes.ui.TemplateSelectActivity;
import net.micode.notes.widget.NoteWidgetProvider_2x;
import net.micode.notes.widget.NoteWidgetProvider_4x;
@ -190,6 +194,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/** 未选中标记 */
public static final String TAG_UNCHECKED = String.valueOf('\u25A1');
/** 模板选择请求码 */
private static final int REQUEST_CODE_TEMPLATE = 1001;
/** 清单模式下的编辑文本列表 */
private LinearLayout mEditTextList;
@ -764,6 +771,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
case R.id.menu_unlock:
showPasswordDialogForUnlock();
break;
case R.id.menu_change_password:
showChangePasswordDialog();
break;
case R.id.menu_note_template:
openTemplateSelector();
break;
default:
break;
}
@ -805,23 +818,41 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/**
*
* <p>
*
*
* </p>
*/
private void showPasswordDialogForLock() {
// 创建布局并添加密码输入框
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(60, 40, 60, 40);
layout.setPadding(60, 40, 60, 40);
// 设置子视图间距
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // 底部外边距作为间距
final EditText passwordEditText = new EditText(this);
passwordEditText.setHint(R.string.hint_enter_password);
passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordEditText);
// 添加密码提示输入框
final EditText hintEditText = new EditText(this);
hintEditText.setHint(R.string.hint_enter_password_hint);
layout.addView(hintEditText);
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_enter_password)
.setView(passwordEditText)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password = passwordEditText.getText().toString();
String hint = hintEditText.getText().toString();
if (!TextUtils.isEmpty(password)) {
lockCurrentNote(password);
lockCurrentNote(password, hint);
}
}
})
@ -836,13 +867,50 @@ public class NoteEditActivity extends Activity implements OnClickListener,
* </p>
*/
private void showPasswordDialogForUnlock() {
// 查询当前笔记的密码提示
String[] projection = {NoteColumns.LOCK_HINT};
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = {String.valueOf(mWorkingNote.getNoteId())};
Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI, projection, selection, selectionArgs, null);
String lockHint = null;
if (cursor != null && cursor.moveToFirst()) {
lockHint = cursor.getString(0);
cursor.close();
}
// 创建布局并添加密码输入框
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(60, 40, 60, 40);
// 创建布局参数,用于设置子视图间距
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // 底部外边距作为间距
final EditText passwordEditText = new EditText(this);
passwordEditText.setHint(R.string.hint_enter_password);
passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordEditText, params);
// 如果有密码提示,显示提示信息
if (!TextUtils.isEmpty(lockHint)) {
TextView hintTextView = new TextView(this);
hintTextView.setText(getString(R.string.password_hint_prefix) + lockHint);
hintTextView.setTextColor(getResources().getColor(R.color.secondary_text_dark));
hintTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
// 提示文本不需要底部间距
LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(hintTextView, hintParams);
}
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_enter_password)
.setView(passwordEditText)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@ -859,17 +927,20 @@ public class NoteEditActivity extends Activity implements OnClickListener,
/**
*
* <p>
*
*
* </p>
* @param password
* @param hint
*/
private void lockCurrentNote(String password) {
private void lockCurrentNote(String password, String hint) {
// 设置锁定状态为1已锁定
mWorkingNote.setNoteValue(NoteColumns.IS_LOCKED, "1");
// 设置加密后的密码
mWorkingNote.setNoteValue(NoteColumns.LOCK_PASSWORD, encryptPassword(password));
// 设置锁定类型为笔记类型
mWorkingNote.setNoteValue(NoteColumns.LOCK_TYPE, String.valueOf(Notes.LOCK_TYPE_NOTE));
// 设置密码提示
mWorkingNote.setNoteValue(NoteColumns.LOCK_HINT, hint);
// 保存笔记
saveNote();
// 显示提示信息
@ -934,6 +1005,97 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return password; // 加密失败时返回原始密码
}
}
/**
*
* <p>
*
* </p>
*/
private void showChangePasswordDialog() {
// 创建布局并添加输入框
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(60, 40, 60, 40);
// 创建布局参数,用于设置子视图间距
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // 底部外边距作为间距
// 旧密码输入框
final EditText oldPasswordEditText = new EditText(this);
oldPasswordEditText.setHint(R.string.hint_enter_password);
oldPasswordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(oldPasswordEditText, params);
// 新密码输入框
final EditText newPasswordEditText = new EditText(this);
newPasswordEditText.setHint(R.string.hint_enter_password);
newPasswordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(newPasswordEditText, params);
// 新密码提示输入框
final EditText newHintEditText = new EditText(this);
newHintEditText.setHint(R.string.hint_enter_password_hint);
layout.addView(newHintEditText);
new AlertDialog.Builder(this)
.setTitle(R.string.menu_change_password)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String oldPassword = oldPasswordEditText.getText().toString();
String newPassword = newPasswordEditText.getText().toString();
String newHint = newHintEditText.getText().toString();
if (!TextUtils.isEmpty(oldPassword) && !TextUtils.isEmpty(newPassword)) {
changePassword(oldPassword, newPassword, newHint);
}
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
/**
*
* <p>
*
* </p>
* @param oldPassword
* @param newPassword
* @param newHint
*/
private void changePassword(String oldPassword, String newPassword, String newHint) {
// 使用ContentResolver直接查询数据库获取加密密码
String[] projection = {NoteColumns.LOCK_PASSWORD};
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = {String.valueOf(mWorkingNote.getNoteId())};
Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI, projection, selection, selectionArgs, null);
String encryptedPassword = null;
if (cursor != null && cursor.moveToFirst()) {
encryptedPassword = cursor.getString(0);
cursor.close();
}
// 验证旧密码是否正确
if (encryptedPassword != null && encryptedPassword.equals(encryptPassword(oldPassword))) {
// 旧密码正确,更新新密码和密码提示
mWorkingNote.setNoteValue(NoteColumns.LOCK_PASSWORD, encryptPassword(newPassword));
mWorkingNote.setNoteValue(NoteColumns.LOCK_HINT, newHint);
// 保存笔记
saveNote();
// 显示提示信息
Toast.makeText(this, getString(R.string.message_password_changed), Toast.LENGTH_SHORT).show();
} else {
// 旧密码错误,显示错误信息
Toast.makeText(this, getString(R.string.error_wrong_password), Toast.LENGTH_SHORT).show();
}
}
/**
*
@ -1375,6 +1537,55 @@ public class NoteEditActivity extends Activity implements OnClickListener,
showToast(resId, Toast.LENGTH_SHORT);
}
/**
*
* <p>
* TemplateSelectActivity便
* </p>
*/
private void openTemplateSelector() {
// 获取当前笔记内容
getWorkingText();
String currentContent = mWorkingNote.getContent();
Intent intent = new Intent(this, TemplateSelectActivity.class);
intent.putExtra(TemplateSelectActivity.EXTRA_CURRENT_NOTE_CONTENT, currentContent);
startActivityForResult(intent, REQUEST_CODE_TEMPLATE);
}
/**
*
* <p>
* TemplateSelectActivity
*
* </p>
* @param requestCode
* @param resultCode
* @param data Intent
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_TEMPLATE && resultCode == RESULT_OK) {
if (data != null) {
String templateContent = data.getStringExtra(TemplateSelectActivity.EXTRA_TEMPLATE_CONTENT);
if (!TextUtils.isEmpty(templateContent)) {
// 保存模板内容到WorkingNote防止onResume时被覆盖
mWorkingNote.setWorkingText(templateContent);
// 更新笔记内容
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(templateContent);
} else {
mNoteEditor.setText(Html.fromHtml(templateContent));
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
Toast.makeText(this, R.string.notealert_enter, Toast.LENGTH_SHORT).show();
}
}
}
}
/**
* Toast
* <p>

@ -57,6 +57,7 @@ public class NoteItemData {
NoteColumns.IS_LOCKED,
NoteColumns.LOCK_PASSWORD,
NoteColumns.LOCK_TYPE,
NoteColumns.LOCK_HINT,
NoteColumns.DELETED_DATE,
};
@ -140,10 +141,15 @@ public class NoteItemData {
*/
private static final int LOCK_TYPE_COLUMN = 15;
/**
*
*/
private static final int LOCK_HINT_COLUMN = 16;
/**
*
*/
private static final int DELETED_DATE_COLUMN = 16;
private static final int DELETED_DATE_COLUMN = 17;
/**
* ID
@ -170,6 +176,11 @@ public class NoteItemData {
*/
private int mLockType;
/**
*
*/
private String mLockHint;
/**
*
*/
@ -290,6 +301,7 @@ public class NoteItemData {
mIsLocked = (cursor.getInt(IS_LOCKED_COLUMN) > 0) ? true : false;
mLockPassword = cursor.getString(LOCK_PASSWORD_COLUMN);
mLockType = cursor.getInt(LOCK_TYPE_COLUMN);
mLockHint = cursor.getString(LOCK_HINT_COLUMN);
mDeletedDate = cursor.getLong(DELETED_DATE_COLUMN);
mPhoneNumber = "";
@ -565,6 +577,22 @@ public class NoteItemData {
return mLockType;
}
/**
*
* @return
*/
public String getLockHint() {
return mLockHint;
}
/**
*
* @param hint
*/
public void setLockHint(String hint) {
mLockHint = hint;
}
/**
*
* @param type 01

@ -57,8 +57,10 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.util.TypedValue;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
@ -124,19 +126,37 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
* </p>
*/
private void showPasswordDialog() {
// 创建布局并添加输入框
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(60, 40, 60, 40);
// 创建布局参数,用于设置子视图间距
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // 底部外边距作为间距
final EditText passwordEditText = new EditText(this);
passwordEditText.setHint(R.string.hint_enter_password);
passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordEditText, params);
// 添加密码提示输入框
final EditText hintEditText = new EditText(this);
hintEditText.setHint(R.string.hint_enter_password_hint);
layout.addView(hintEditText);
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_enter_password)
.setView(passwordEditText)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String password = passwordEditText.getText().toString();
String hint = hintEditText.getText().toString();
if (!TextUtils.isEmpty(password)) {
toggleLockedStatus(password);
toggleLockedStatus(password, hint);
}
}
})
@ -150,8 +170,9 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
* 使
* </p>
* @param password
* @param hint
*/
private void toggleLockedStatus(final String password) {
private void toggleLockedStatus(final String password, final String hint) {
final HashSet<Long> selectedIds = mNotesListAdapter.getSelectedItemIds();
final int selectedCount = mNotesListAdapter.getSelectedCount();
@ -159,45 +180,61 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
protected Integer doInBackground(Void... unused) {
int finalLockedStatus = -1; // 默认为-1表示未处理
for (Long noteId : selectedIds) {
// 查询当前便签的锁定状态
// 查询当前便签的锁定状态和密码
Cursor cursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.IS_LOCKED},
new String[]{NoteColumns.IS_LOCKED, NoteColumns.LOCK_PASSWORD},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null);
if (cursor != null && cursor.moveToFirst()) {
int currentLocked = cursor.getInt(0);
// 切换锁定状态
int newLocked = currentLocked == 1 ? 0 : 1;
finalLockedStatus = newLocked; // 保存最终锁定状态
ContentValues values = new ContentValues();
values.put(NoteColumns.IS_LOCKED, newLocked);
if (newLocked == 1) {
// 如果是锁定操作,设置密码和锁定类型
values.put(NoteColumns.LOCK_PASSWORD, encryptPassword(password));
// 判断是笔记还是文件夹
Cursor typeCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.TYPE},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null);
if (typeCursor != null && typeCursor.moveToFirst()) {
int type = typeCursor.getInt(0);
values.put(NoteColumns.LOCK_TYPE, type == Notes.TYPE_FOLDER ? Notes.LOCK_TYPE_FOLDER : Notes.LOCK_TYPE_NOTE);
typeCursor.close();
String encryptedPassword = cursor.getString(1);
int newLocked = currentLocked;
if (currentLocked == 1) {
// 如果当前是锁定状态,需要验证密码才能解锁
if (encryptedPassword != null && encryptedPassword.equals(encryptPassword(password))) {
// 密码正确,解锁
newLocked = 0;
finalLockedStatus = newLocked;
}
} else {
// 如果是解锁操作,清空密码
values.put(NoteColumns.LOCK_PASSWORD, "");
// 如果当前是未锁定状态,直接锁定
newLocked = 1;
finalLockedStatus = newLocked;
}
mContentResolver.update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)});
if (newLocked != currentLocked) {
ContentValues values = new ContentValues();
values.put(NoteColumns.IS_LOCKED, newLocked);
if (newLocked == 1) {
// 如果是锁定操作,设置密码、密码提示和锁定类型
values.put(NoteColumns.LOCK_PASSWORD, encryptPassword(password));
values.put(NoteColumns.LOCK_HINT, hint);
// 判断是笔记还是文件夹
Cursor typeCursor = mContentResolver.query(Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.TYPE},
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)},
null);
if (typeCursor != null && typeCursor.moveToFirst()) {
int type = typeCursor.getInt(0);
values.put(NoteColumns.LOCK_TYPE, type == Notes.TYPE_FOLDER ? Notes.LOCK_TYPE_FOLDER : Notes.LOCK_TYPE_NOTE);
typeCursor.close();
}
} else {
// 如果是解锁操作,清空密码和密码提示
values.put(NoteColumns.LOCK_PASSWORD, "");
values.put(NoteColumns.LOCK_HINT, "");
}
mContentResolver.update(Notes.CONTENT_NOTE_URI,
values,
NoteColumns.ID + "=?",
new String[]{String.valueOf(noteId)});
}
cursor.close();
}
@ -207,11 +244,17 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
@Override
protected void onPostExecute(Integer finalLockedStatus) {
String message = finalLockedStatus == 1 ? getString(R.string.message_note_locked) : getString(R.string.message_note_unlocked);
Toast.makeText(NotesListActivity.this, message, Toast.LENGTH_SHORT).show();
// 重新查询数据,更新列表
startAsyncNotesListQuery();
mModeCallBack.finishActionMode();
if (finalLockedStatus == -1) {
// 密码错误,显示错误信息
Toast.makeText(NotesListActivity.this, getString(R.string.error_wrong_password), Toast.LENGTH_SHORT).show();
} else {
// 操作成功,显示结果
String message = finalLockedStatus == 1 ? getString(R.string.message_note_locked) : getString(R.string.message_note_unlocked);
Toast.makeText(NotesListActivity.this, message, Toast.LENGTH_SHORT).show();
// 重新查询数据,更新列表
startAsyncNotesListQuery();
mModeCallBack.finishActionMode();
}
}
}.execute();
}
@ -1193,13 +1236,35 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private void openNode(final NoteItemData data) {
// 检查笔记是否被锁定
if (data.isLocked()) {
// 创建布局并添加密码输入框
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(60, 40, 60, 40);
// 创建布局参数,用于设置子视图间距
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20); // 底部外边距作为间距
final EditText passwordEditText = new EditText(this);
passwordEditText.setHint(R.string.hint_enter_password);
passwordEditText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordEditText, params);
// 获取密码提示并显示
String lockHint = data.getLockHint();
if (!TextUtils.isEmpty(lockHint)) {
TextView hintTextView = new TextView(this);
hintTextView.setText(getString(R.string.password_hint_prefix) + lockHint);
hintTextView.setTextColor(getResources().getColor(R.color.secondary_text_dark));
hintTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
layout.addView(hintTextView);
}
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_enter_password)
.setView(passwordEditText)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

@ -99,20 +99,34 @@ public class NotesListItem extends LinearLayout {
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);
// 检查是否被锁定
if (data.isLocked()) {
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
} else {
mAlert.setImageResource(R.drawable.call_record);
}
} else if (data.getId() == Notes.ID_TRASH_FOLER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText("回收站"
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.delete);
// 检查是否被锁定
if (data.isLocked()) {
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
} else {
mAlert.setImageResource(R.drawable.delete);
}
} 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(Html.fromHtml(DataUtils.getFormattedSnippet(data.getSnippet())));
if (data.hasAlert()) {
// 检查是否被锁定
if (data.isLocked()) {
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
mAlert.setVisibility(View.VISIBLE);
} else if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {
@ -126,10 +140,20 @@ public class NotesListItem extends LinearLayout {
mTitle.setText(data.getSnippet()
+ context.getString(R.string.format_folder_files_count,
data.getNotesCount()));
mAlert.setVisibility(View.GONE);
// 检查文件夹是否被锁定
if (data.isLocked()) {
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
mAlert.setVisibility(View.VISIBLE);
} else {
mAlert.setVisibility(View.GONE);
}
} else {
mTitle.setText(Html.fromHtml(DataUtils.getFormattedSnippet(data.getSnippet())));
if (data.hasAlert()) {
// 检查笔记是否被锁定
if (data.isLocked()) {
mAlert.setImageResource(android.R.drawable.ic_lock_lock);
mAlert.setVisibility(View.VISIBLE);
} else if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
mAlert.setVisibility(View.VISIBLE);
} else {

@ -0,0 +1,207 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.model.NoteTemplate;
import net.micode.notes.model.TemplateManager;
import java.util.List;
/**
* Activity
*/
public class TemplateSelectActivity extends Activity {
public static final String EXTRA_TEMPLATE_CONTENT = "template_content";
public static final String EXTRA_CURRENT_NOTE_CONTENT = "current_note_content";
private RecyclerView mRecyclerView;
private TemplateAdapter mAdapter;
private List<NoteTemplate> mTemplates;
private TemplateManager mTemplateManager;
private String mCurrentNoteContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.template_select);
// 获取当前笔记内容
mCurrentNoteContent = getIntent().getStringExtra(EXTRA_CURRENT_NOTE_CONTENT);
// 初始化模板管理器
mTemplateManager = TemplateManager.getInstance(this);
mTemplates = mTemplateManager.getAllTemplates();
// 初始化RecyclerView
mRecyclerView = findViewById(R.id.rv_templates);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new TemplateAdapter(mTemplates);
mRecyclerView.setAdapter(mAdapter);
// 初始化按钮点击事件
findViewById(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
findViewById(R.id.btn_save_as_template).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
saveAsTemplate();
}
});
}
/**
*
*/
private void saveAsTemplate() {
if (TextUtils.isEmpty(mCurrentNoteContent)) {
Toast.makeText(this, R.string.error_note_empty, Toast.LENGTH_SHORT).show();
return;
}
// 弹出对话框输入模板名称
final EditText editText = new EditText(this);
editText.setHint(R.string.enter_template_name);
new AlertDialog.Builder(this)
.setTitle(R.string.save_as_template)
.setView(editText)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String templateName = editText.getText().toString().trim();
if (TextUtils.isEmpty(templateName)) {
Toast.makeText(TemplateSelectActivity.this, R.string.error_template_name_empty, Toast.LENGTH_SHORT).show();
return;
}
// 创建并保存自定义模板
NoteTemplate template = new NoteTemplate();
template.setName(templateName);
template.setContent(mCurrentNoteContent);
template.setType(NoteTemplate.TYPE_CUSTOM);
mTemplateManager.addCustomTemplate(template);
// 更新模板列表
mTemplates = mTemplateManager.getAllTemplates();
mAdapter = new TemplateAdapter(mTemplates);
mRecyclerView.setAdapter(mAdapter);
Toast.makeText(TemplateSelectActivity.this, R.string.message_template_saved, Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
/**
* RecyclerView
*/
private class TemplateAdapter extends RecyclerView.Adapter<TemplateAdapter.ViewHolder> {
private List<NoteTemplate> mTemplates;
public TemplateAdapter(List<NoteTemplate> templates) {
mTemplates = templates;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.template_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final NoteTemplate template = mTemplates.get(position);
holder.tvTemplateName.setText(template.getName());
// 设置模板类型标识
if (NoteTemplate.TYPE_SYSTEM.equals(template.getType())) {
holder.tvTemplateType.setText(R.string.template_type_system);
holder.ivDelete.setVisibility(View.GONE);
} else {
holder.tvTemplateType.setText(R.string.template_type_custom);
holder.ivDelete.setVisibility(View.VISIBLE);
}
// 设置点击事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 返回选择的模板内容
Intent intent = new Intent();
intent.putExtra(EXTRA_TEMPLATE_CONTENT, template.getContent());
setResult(RESULT_OK, intent);
finish();
}
});
// 设置删除按钮点击事件
holder.ivDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(TemplateSelectActivity.this)
.setTitle(R.string.alert_title_delete)
.setMessage(R.string.alert_message_delete_template)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// 删除自定义模板
mTemplateManager.deleteCustomTemplate(template.getId());
// 更新模板列表
mTemplates.clear();
mTemplates.addAll(mTemplateManager.getAllTemplates());
notifyDataSetChanged();
Toast.makeText(TemplateSelectActivity.this, R.string.message_template_deleted, Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
});
}
@Override
public int getItemCount() {
return mTemplates.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
public TextView tvTemplateName;
public TextView tvTemplateType;
public ImageView ivDelete;
public ViewHolder(View itemView) {
super(itemView);
tvTemplateName = itemView.findViewById(R.id.tv_template_name);
tvTemplateType = itemView.findViewById(R.id.tv_template_type);
ivDelete = itemView.findViewById(R.id.iv_delete);
}
}
}
}

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#E0E0E0" />
<stroke
android:width="1dp"
android:color="#BDBDBD" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#FFFFFF" />
<stroke
android:width="1dp"
android:color="#BDBDBD" />
</shape>
</item>
</selector>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="12dp" />
<solid android:color="#F0F0F0" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:background="@drawable/template_item_bg"
android:layout_marginBottom="8dp"
android:gravity="center_vertical">
<!-- 模板名称 -->
<TextView
android:id="@+id/tv_template_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@android:color/black"
android:fontFamily="sans-serif-medium" />
<!-- 模板类型标识 -->
<TextView
android:id="@+id/tv_template_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:textSize="12sp"
android:textColor="@android:color/secondary_text_light"
android:background="@drawable/template_type_bg"
android:paddingHorizontal="8dp"
android:paddingVertical="2dp"
android:text="@string/template_type_system" />
<!-- 仅自定义模板显示删除按钮 -->
<ImageView
android:id="@+id/iv_delete"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginLeft="8dp"
android:src="@android:drawable/ic_menu_delete"
android:tint="@android:color/darker_gray"
android:visibility="gone" />
</LinearLayout>

@ -0,0 +1,59 @@
<?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">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?android:attr/colorPrimary"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/menu_template"
android:textSize="20sp"
android:textColor="@android:color/white"
android:fontFamily="sans-serif-medium" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
android:textColor="@android:color/white"
android:background="@null" />
</LinearLayout>
<!-- 模板列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_templates"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="16dp" />
<!-- 添加自定义模板按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:orientation="vertical">
<Button
android:id="@+id/btn_save_as_template"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/save_as_template"
android:background="?android:attr/colorPrimary"
android:textColor="@android:color/white"
android:padding="12dp"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>

@ -57,4 +57,12 @@
<item
android:id="@+id/menu_unlock"
android:title="@string/menu_unlock" />
<item
android:id="@+id/menu_change_password"
android:title="@string/menu_change_password" />
<item
android:id="@+id/menu_note_template"
android:title="@string/menu_note_template" />
</menu>

@ -52,11 +52,17 @@
<string name="menu_unpin">Unpin</string>
<string name="menu_lock">Lock</string>
<string name="menu_unlock">Unlock</string>
<string name="menu_change_password">Change Password</string>
<string name="dialog_enter_password">Enter Password</string>
<string name="dialog_enter_old_password">Enter Old Password</string>
<string name="dialog_enter_new_password">Enter New Password</string>
<string name="hint_enter_password">Please enter password</string>
<string name="hint_enter_password_hint">Please enter password hint</string>
<string name="password_hint_prefix">Hint: </string>
<string name="error_wrong_password">Incorrect password</string>
<string name="message_note_locked">Note locked successfully</string>
<string name="message_note_unlocked">Note unlocked successfully</string>
<string name="message_password_changed">Password changed successfully</string>
<string name="menu_select_title">%d selected</string>
<string name="menu_select_none">Nothing selected, the operation is invalid</string>
<string name="menu_select_all">Select all</string>
@ -141,9 +147,22 @@
<string name="datetime_dialog_ok">set</string>
<string name="datetime_dialog_cancel">cancel</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>
<!-- Template related strings -->
<string name="template_type_system">System</string>
<string name="template_type_custom">Custom</string>
<string name="menu_template">Templates</string>
<string name="save_as_template">Save as Template</string>
<string name="enter_template_name">Enter template name</string>
<string name="error_note_empty">Note is empty</string>
<string name="error_template_name_empty">Template name cannot be empty</string>
<string name="message_template_saved">Template saved successfully</string>
<string name="message_template_deleted">Template deleted successfully</string>
<string name="alert_message_delete_template">Are you sure you want to delete this template?</string>
<string name="menu_note_template">Note Template</string>
</resources>

Loading…
Cancel
Save