完成了富文本,便签插入图片,便签涂鸦,回收站,漂流瓶,新闹钟,加密文件夹功能

完成了富文本,便签插入图片,便签涂鸦,回收站,漂流瓶,新闹钟,加密文件夹功能
wangyijia_branch
ple74bfj6 1 month ago
commit c2e9e13602

@ -17,40 +17,24 @@
package net.micode.notes.data;
import android.net.Uri;
/**
*
* @Package: net.micode.notes.data
* @ClassName: Notes
* @Description:
*/
public class Notes {
//定义此应用ContentProvider的唯一标识
public static final String AUTHORITY = "net.micode.notes.provider";
public static final String AUTHORITY = "micode_notes";
public static final String TAG = "Notes";
/**
*{@link Notes#TYPE_NOTE } 便
*{@link Notes#TYPE_FOLDER }
*{@link Notes#TYPE_SYSTEM }
*/
public static final int TYPE_NOTE = 0;
public static final int TYPE_FOLDER = 1;
public static final int TYPE_SYSTEM = 2;
/**
* Following IDs are system folders' identifiers
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder 便
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
* {@link Notes#ID_TRASH_FOLER}
* {@link Notes#ID_ROOT_FOLDER } is default folder
* {@link Notes#ID_TEMPARAY_FOLDER } is for notes belonging no folder
* {@link Notes#ID_CALL_RECORD_FOLDER} is to store call records
*/
public static final int ID_ROOT_FOLDER = 0;
public static final int ID_TEMPARAY_FOLDER = -1;
public static final int ID_CALL_RECORD_FOLDER = -2;
public static final int ID_TRASH_FOLER = -3;
//定义Intent Extra 的常量,用于在不同组件之间安全地传递数据
public static final String INTENT_EXTRA_ALERT_DATE = "net.micode.notes.alert_date";
public static final String INTENT_EXTRA_BACKGROUND_ID = "net.micode.notes.background_color_id";
public static final String INTENT_EXTRA_WIDGET_ID = "net.micode.notes.widget_id";
@ -62,10 +46,10 @@ public class Notes {
public static final int TYPE_WIDGET_2X = 0;
public static final int TYPE_WIDGET_4X = 1;
// 将不同数据类型对应到其MIMEitem类型字符串
public static class DataConstants {
public static final String NOTE = TextNote.CONTENT_ITEM_TYPE;
public static final String CALL_NOTE = CallNote.CONTENT_ITEM_TYPE;
public static final String ENCRYPTED_FOLDER = "vnd.android.cursor.item/encrypted_folder";
}
/**
@ -78,7 +62,6 @@ public class Notes {
*/
public static final Uri CONTENT_DATA_URI = Uri.parse("content://" + AUTHORITY + "/data");
//Note表数据库列名常量接口
public interface NoteColumns {
/**
* The unique ID for a row
@ -113,8 +96,6 @@ public class Notes {
/**
* Folder's name or text content of note
*
* 便便
* <P> Type: TEXT </P>
*/
public static final String SNIPPET = "snippet";
@ -187,7 +168,6 @@ public class Notes {
public static final String VERSION = "version";
}
//Data表数据库列名常量接口
public interface DataColumns {
/**
* The unique ID for a row
@ -262,12 +242,10 @@ public class Notes {
public static final String DATA5 = "data5";
}
//DataColumns接口的实现方式1文本便签的数据模型
public static final class TextNote implements DataColumns {
/**
* Mode to indicate the text in check list mode or not
* <P> Type: Integer 1:check list mode 0: normal mode </P>
* data1
*/
public static final String MODE = DATA1;
@ -280,19 +258,16 @@ public class Notes {
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/text_note");
}
//DataColumns接口的实现方式2通话便签的数据模型
public static final class CallNote implements DataColumns {
/**
* Call date for this record
* <P> Type: INTEGER (long) </P>
* data1
*/
public static final String CALL_DATE = DATA1;
/**
* Phone number for this record
* <P> Type: TEXT </P>
* data3
*/
public static final String PHONE_NUMBER = DATA3;

@ -35,7 +35,7 @@ import net.micode.notes.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 5;
private static final int DB_VERSION = 4;
public interface TABLE {
public static final String NOTE = "note";
@ -68,9 +68,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.IS_HABIT + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.HABIT_CONFIG + " TEXT NOT NULL DEFAULT ''" +
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" +
")";
/**datasql
@ -406,11 +404,6 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
if (oldVersion == 4) {
upgradeToV5(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@ -451,12 +444,4 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
//更新到版本5增加习惯打卡支持字段
private void upgradeToV5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.IS_HABIT
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.HABIT_CONFIG
+ " TEXT NOT NULL DEFAULT ''");
}
}

@ -37,7 +37,6 @@ import java.util.HashSet;
public class DataUtils {
public static final String TAG = "DataUtils";
//批量操作删除便签发生错误就返回flase删除成功就返回true
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
if (ids == null) {
Log.d(TAG, "the ids is null");
@ -47,42 +46,41 @@ public class DataUtils {
Log.d(TAG, "no id is in the hashset");
return true;
}
// 构建删除操作拼接标签ID到标签ContentUR
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER) {
Log.e(TAG, "Don't delete system folder root");
continue;
}
ContentProviderOperation.Builder builder = ContentProviderOperation
.newDelete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
//创建一个 ContentProviderOperation.Builder删除指定id的标签 只查询 ID 为 id 的那一条记录
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {////上面的一条执行数据库操作删除并返回结果数组
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "delete notes failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) { //记录错误信息
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
//便签转进新文件夹
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, desFolderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, srcFolderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null);
}
//批量操作便签转进新文件夹
public static boolean batchMoveToFolder(ContentResolver resolver, HashSet<Long> ids,
long folderId) {
if (ids == null) {
@ -93,9 +91,10 @@ public class DataUtils {
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); //创建更新,
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, folderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
builder.withValue(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
operationList.add(builder.build());
}
@ -113,21 +112,11 @@ public class DataUtils {
}
return false;
}
/**
* Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}}
*/
public static int getUserFolderCount(ContentResolver resolver) {
//查询设备中「类型为文件夹TYPE_FOLDER」且「父文件夹 ID 不等于回收站 IDID_TRASH_FOLER」的文件夹总数。
'''
Cursor query(
Uri uri, // 要查询的数据 URI
String[] projection, // 要返回的列字段null 表示所有列
String selection, // WHERE 子句(不含 "WHERE" 关键字)
String[] selectionArgs, // WHERE 子句中占位符 ? 的实际值
String sortOrder // 排序方式(如 "date DESC"
);
'''
public static int getUserFolderCount(ContentResolver resolver) {
Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { "COUNT(*)" },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?",
@ -138,7 +127,7 @@ Cursor query(
if(cursor != null) {
if(cursor.moveToFirst()) {
try {
count = cursor.getInt(0);//从当前游标指向的行中,读取第 0 列(第一列)的整数类型数据
count = cursor.getInt(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "get folder count failed:" + e.toString());
} finally {
@ -148,11 +137,11 @@ Cursor query(
}
return count;
}
//变迁是否可见(没进回收站)
public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null,
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, //笔记的 parent_id 不能等于回收站的 ID即不在回收站中
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER,
new String [] {String.valueOf(type)},
null);
@ -165,7 +154,7 @@ Cursor query(
}
return exist;
}
//便签还存在否
public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
null, null, null, null);
@ -179,7 +168,7 @@ Cursor query(
}
return exist;
}
//便签数据还存在否
public static boolean existInDataDatabase(ContentResolver resolver, long dataId) {
Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId),
null, null, null, null);
@ -193,12 +182,12 @@ Cursor query(
}
return exist;
}
//查看是否存在一个名字是XX的文件夹
public static boolean checkVisibleFolderName(ContentResolver resolver, String name) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, null,
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + //类型是文件夹
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +//没进回收站
" AND " + NoteColumns.SNIPPET + "=?", //存储在snippet的名字和下面的name一样
NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER +
" AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER +
" AND " + NoteColumns.SNIPPET + "=?",
new String[] { name }, null);
boolean exist = false;
if(cursor != null) {
@ -209,9 +198,9 @@ Cursor query(
}
return exist;
}
//获得文件夹下的便签,并仅返回它们的 Widget ID 和 Widget 类型
public static HashSet<AppWidgetAttribute> getFolderNoteWidget(ContentResolver resolver, long folderId) {
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, //获得便签
Cursor c = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE },
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(folderId) },
@ -236,7 +225,7 @@ Cursor query(
}
return set;
}
//获得电话号
public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.PHONE_NUMBER },
@ -246,7 +235,7 @@ Cursor query(
if (cursor != null && cursor.moveToFirst()) {
try {
return cursor.getString(0); //返回电话号
return cursor.getString(0);
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get call number fails " + e.toString());
} finally {
@ -255,7 +244,7 @@ Cursor query(
}
return "";
}
//通过电话号和日期查找便签
public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) {
Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI,
new String [] { CallNote.NOTE_ID },
@ -276,7 +265,7 @@ Cursor query(
}
return 0;
}
//通过id获得名字
public static String getSnippetById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.SNIPPET },
@ -294,15 +283,89 @@ Cursor query(
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
//从一段文本snippet中提取作为标题
public static String getFormattedSnippet(String snippet) {
if (snippet != null) {
snippet = snippet.trim();//清空无用制表符
int index = snippet.indexOf('\n');//index即录字符串中第一个换行符 \n 出现的位置
snippet = snippet.trim();
int index = snippet.indexOf('\n');
if (index != -1) {
snippet = snippet.substring(0, index);//只获取换行前的部分(核心)。
snippet = snippet.substring(0, index);
}
}
return snippet;
}
/**
* 便ID
* @param resolver ContentResolver
* @param folderId ID
* @return 便IDHashSet
*/
public static HashSet<Long> getNotesInFolder(ContentResolver resolver, long folderId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.ID },
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(folderId) },
null);
HashSet<Long> ids = new HashSet<Long>();
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
try {
ids.add(cursor.getLong(0));
} catch (IndexOutOfBoundsException e) {
Log.e(TAG, "Get note id fails " + e.toString());
}
} while (cursor.moveToNext());
}
cursor.close();
}
return ids;
}
public static boolean batchMoveToTrash(ContentResolver resolver, HashSet<Long> ids,
long originFolderId) {
if (ids == null) {
Log.d(TAG, "the ids is null");
return true;
}
ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
long now = System.currentTimeMillis();
for (long id : ids) {
ContentProviderOperation.Builder builder = ContentProviderOperation
.newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id));
builder.withValue(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
builder.withValue(NoteColumns.ORIGIN_PARENT_ID, originFolderId);
builder.withValue(NoteColumns.LOCAL_MODIFIED, 1);
builder.withValue(NoteColumns.MODIFIED_DATE, now);
operationList.add(builder.build());
}
try {
ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList);
if (results == null || results.length == 0 || results[0] == null) {
Log.d(TAG, "move to trash failed, ids:" + ids.toString());
return false;
}
return true;
} catch (RemoteException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
} catch (OperationApplicationException e) {
Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
}
return false;
}
public static void moveNotesToTrashForFolder(ContentResolver resolver, long folderId) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.ORIGIN_PARENT_ID, folderId);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
resolver.update(Notes.CONTENT_NOTE_URI, values,
NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(folderId) });
}
}

@ -1,162 +0,0 @@
package net.micode.notes.tool;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.util.Locale;
public class TranslateUtils {
private static final String TAG = "TranslateUtils";
private static final String YOUDAO_APP_KEY = "3abfa533dbdc44d1";
private static final String YOUDAO_APP_SECRET = "aliNHKWhhTlaLjRAkOce4cHTubriEl0c";
private static final String YOUDAO_URL = "https://openapi.youdao.com/api";
public static boolean isOnline(Context ctx) {
ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = cm.getActiveNetwork();
if (network == null) return false;
NetworkCapabilities capabilities = cm.getNetworkCapabilities(network);
return capabilities != null && (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET));
} else {
// 兼容低版本
NetworkInfo ni = cm.getActiveNetworkInfo();
return ni != null && ni.isConnected();
}
}
public static String translateParagraph(String text, String targetLang) {
if (TextUtils.isEmpty(YOUDAO_APP_KEY) || TextUtils.isEmpty(YOUDAO_APP_SECRET)) {
Log.w(TAG, "Youdao app key/secret not configured");
return null;
}
try {
Log.d(TAG, "Starting translation: text=" + text + ", targetLang=" + targetLang);
String q = text;
String from = "auto";
String to = targetLang == null ? "en" : targetLang;
String salt = String.valueOf(System.currentTimeMillis());
String sign = md5(YOUDAO_APP_KEY + q + salt + YOUDAO_APP_SECRET);
StringBuilder sb = new StringBuilder();
sb.append("appKey=").append(urlEncode(YOUDAO_APP_KEY));
sb.append("&q=").append(urlEncode(q));
sb.append("&salt=").append(urlEncode(salt));
sb.append("&from=").append(urlEncode(from));
sb.append("&to=").append(urlEncode(to));
sb.append("&sign=").append(urlEncode(sign));
URL url = new URL(YOUDAO_URL);
Log.d(TAG, "Connecting to: " + YOUDAO_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
writer.write(sb.toString());
writer.flush();
writer.close();
int code = conn.getResponseCode();
Log.d(TAG, "Response code: " + code);
if (code != 200) {
Log.w(TAG, "Youdao response code:" + code);
// 读取错误响应
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8"))) {
StringBuilder errorResp = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
errorResp.append(line);
}
Log.w(TAG, "Error response: " + errorResp.toString());
} catch (Exception e) {
Log.w(TAG, "Failed to read error response", e);
}
return null;
}
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
StringBuilder resp = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
resp.append(line);
}
br.close();
conn.disconnect();
Log.d(TAG, "Response: " + resp.toString());
JSONObject json = new JSONObject(resp.toString());
if (json.has("translation")) {
JSONArray arr = json.getJSONArray("translation");
if (arr.length() > 0) {
String result = arr.getString(0);
Log.d(TAG, "Translation result: " + result);
return result;
}
}
// fallback: try webdict or basic
if (json.has("web") && json.getJSONArray("web").length() > 0) {
JSONObject w = json.getJSONArray("web").getJSONObject(0);
if (w.has("value")) {
JSONArray v = w.getJSONArray("value");
if (v.length() > 0) {
String result = v.getString(0);
Log.d(TAG, "Web dict fallback result: " + result);
return result;
}
}
}
Log.w(TAG, "No translation found in response");
return null;
} catch (Exception e) {
Log.w(TAG, "translate error: " + e.getMessage(), e);
return null;
}
}
private static String urlEncode(String s) throws Exception {
return java.net.URLEncoder.encode(s, "UTF-8");
}
private static String md5(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(s.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xff);
if (hex.length() == 1) sb.append('0');
sb.append(hex);
}
return sb.toString();
}
}

@ -18,16 +18,11 @@ package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.AlarmManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.database.Cursor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
@ -35,29 +30,17 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import java.io.IOException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Calendar;
//继承Activity //承诺实现clickdismiss接口不继承功能
public class AlarmAlertActivity extends Activity implements OnClickListener, OnDismissListener {
private static final String TAG = "AlarmAlertActivity";
private long mNoteId;
private String mSnippet;
private static final int SNIPPET_PREW_MAX_LEN = 60;
@ -100,12 +83,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
mPlayer = new MediaPlayer();
//如果不在回收站就显示文字并发出声音
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
int isHabit = intent.getIntExtra("habit_alarm", 0);
if (isHabit == 1) {
showHabitDialog(intent);
} else {
showActionDialog();
}
showActionDialog();
playAlarmSound();
} else {
finish();
@ -156,190 +134,11 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
dialog.setMessage(mSnippet);
dialog.setPositiveButton(R.string.notealert_ok, this); //setPositive/Negative/NeutralButton()设置:确定,取消,中立按钮;
if (isScreenOn()) {
dialog.setNegativeButton(R.string.notealert_enter, this);
dialog.setNegativeButton(R.string.notealert_enter, this); //取消
}
dialog.show().setOnDismissListener(this);
dialog.show().setOnDismissListener(this); //设置关闭监听
}
// Habit specific dialog with actions: complete, snooze, skip, abandon
private void showHabitDialog(Intent intent) {
View v = getLayoutInflater().inflate(R.layout.habit_alert_dialog, null);
TextView tvTitle = (TextView) v.findViewById(R.id.habit_alert_title);
TextView tvSnippet = (TextView) v.findViewById(R.id.habit_alert_snippet);
final Button btnComplete = (Button) v.findViewById(R.id.habit_btn_complete);
final Button btnSnooze10 = (Button) v.findViewById(R.id.habit_btn_snooze10);
final Button btnSnooze30 = (Button) v.findViewById(R.id.habit_btn_snooze30);
final Button btnSkip = (Button) v.findViewById(R.id.habit_btn_skip);
final Button btnAbandon = (Button) v.findViewById(R.id.habit_btn_abandon);
tvTitle.setText(getString(R.string.app_name));
tvSnippet.setText(mSnippet);
final AlertDialog d = new AlertDialog.Builder(this)
.setView(v)
.create();
btnComplete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recordHabitHistory(mNoteId, "completed", "");
Toast.makeText(AlarmAlertActivity.this, R.string.habit_record_complete, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSnooze10.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scheduleSnooze(mNoteId, 10);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_snoozed, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSnooze30.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scheduleSnooze(mNoteId, 30);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_snoozed, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
btnSkip.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showSkipReasonDialog();
// 不要立即关闭对话框,让用户选择原因
}
});
btnAbandon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
abandonHabit(mNoteId);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_abandoned, Toast.LENGTH_SHORT).show();
d.dismiss();
}
});
d.setOnDismissListener(this);
d.show();
}
private void showSkipReasonDialog() {
final String[] reasons = new String[] { getString(R.string.skip_reason_busy),
getString(R.string.skip_reason_sick), getString(R.string.skip_reason_other) };
AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(R.string.habit_skip_title);
b.setItems(reasons, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String reason = reasons[which];
recordHabitHistory(mNoteId, "skipped", reason);
Toast.makeText(AlarmAlertActivity.this, R.string.habit_record_skipped, Toast.LENGTH_SHORT).show();
// 选择原因后关闭主对话框
dialog.dismiss();
finish();
}
});
b.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
b.show();
}
private void scheduleSnooze(long noteId, int minutes) {
try {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
int req = (int) (noteId ^ 0x100000) + minutes; // unique-ish
PendingIntent pi = PendingIntent.getBroadcast(this, req, intent, 0);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
long trigger = System.currentTimeMillis() + minutes * 60 * 1000L;
am.set(AlarmManager.RTC_WAKEUP, trigger, pi);
} catch (Exception e) {
Log.e(TAG, "Schedule snooze error", e);
}
}
private void abandonHabit(long noteId) {
try {
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.IS_HABIT, 0);
values.put(Notes.NoteColumns.HABIT_CONFIG, "");
getContentResolver().update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), values, null, null);
// cancel repeating alarm
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId));
intent.putExtra("habit_alarm", 1);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, 0);
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
am.cancel(pi);
} catch (Exception e) {
Log.e(TAG, "Abandon habit error", e);
}
}
// Record history into habit_config.history (append object {date,status,reason})
private void recordHabitHistory(long noteId, String status, String reason) {
try {
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
Cursor c = getContentResolver().query(uri, new String[]{Notes.NoteColumns.HABIT_CONFIG}, null, null, null);
String cfg = "";
if (c != null) {
if (c.moveToFirst()) cfg = c.getString(0);
c.close();
}
JSONObject jo = cfg != null && cfg.length() > 0 ? new JSONObject(cfg) : new JSONObject();
JSONArray history = jo.has("history") ? jo.getJSONArray("history") : new JSONArray();
// 获取当前时间戳
long recordTime = System.currentTimeMillis();
// 创建新记录
JSONObject newEntry = new JSONObject();
newEntry.put("date", recordTime);
newEntry.put("status", status);
newEntry.put("reason", reason == null ? "" : reason);
// 检查是否已经存在该日期的记录,如果有则替换
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
String recordKey = sdf.format(new java.util.Date(recordTime));
boolean foundRecord = false;
for (int i = 0; i < history.length(); i++) {
JSONObject entry = history.getJSONObject(i);
long entryDate = entry.optLong("date", 0);
String entryKey = sdf.format(new java.util.Date(entryDate));
if (recordKey.equals(entryKey)) {
// 替换该日期的记录
history.put(i, newEntry);
foundRecord = true;
break;
}
}
// 如果没有该日期的记录,添加新记录
if (!foundRecord) {
history.put(newEntry);
}
jo.put("history", history);
ContentValues values = new ContentValues();
values.put(Notes.NoteColumns.HABIT_CONFIG, jo.toString());
getContentResolver().update(uri, values, null, null);
// 通知数据变化,以便日历视图刷新
getContentResolver().notifyChange(uri, null);
} catch (JSONException e) {
Log.e(TAG, "Record habit history json error", e);
}
}
//选择negative时执行跳转到笔记 那可能positive仅关闭弹窗
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case DialogInterface.BUTTON_NEGATIVE:

@ -1,131 +0,0 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.widget.Toast;
import android.view.View;
import net.micode.notes.R;
import java.io.InputStream;
public class BackgroundManager {
public static final int REQUEST_CODE_PICK_IMAGE = 2001;
private static final String PREF_BG_TYPE = "pref_bg_type"; // 0=color,1=builtin,2=uri
private static final String PREF_BG_COLOR = "pref_bg_color";
private static final String PREF_BG_RES_ID = "pref_bg_res_id";
private static final String PREF_BG_URI = "pref_bg_uri";
private static final int BG_TYPE_COLOR = 0;
private static final int BG_TYPE_BUILTIN = 1;
private static final int BG_TYPE_URI = 2;
private Activity mActivity;
private View mRootView;
public BackgroundManager(Activity activity, int rootViewId) {
mActivity = activity;
mRootView = activity.findViewById(rootViewId);
if (mRootView == null) {
mRootView = activity.getWindow().getDecorView();
}
}
public void applyBackgroundFromPrefs() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
int type = sp.getInt(PREF_BG_TYPE, -1);
if (type == BG_TYPE_COLOR) {
int color = sp.getInt(PREF_BG_COLOR, 0xFFFFFFFF);
applyColorBackground(color);
} else if (type == BG_TYPE_BUILTIN) {
int resId = sp.getInt(PREF_BG_RES_ID, R.drawable.list_background);
applyBuiltinBackground(resId);
} else if (type == BG_TYPE_URI) {
String uriStr = sp.getString(PREF_BG_URI, null);
if (uriStr != null) {
applyUriBackground(Uri.parse(uriStr));
}
} else {
mRootView.setBackgroundResource(R.drawable.list_background);
}
}
public void applyColorAndSave(int color) {
applyColorBackground(color);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
sp.edit().putInt(PREF_BG_TYPE, BG_TYPE_COLOR).putInt(PREF_BG_COLOR, color).commit();
}
public void applyBuiltinAndSave(int resId) {
applyBuiltinBackground(resId);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
sp.edit().putInt(PREF_BG_TYPE, BG_TYPE_BUILTIN).putInt(PREF_BG_RES_ID, resId).commit();
}
public void pickImageFromGallery() {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
mActivity.startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
}
public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
Uri uri = data.getData();
if (uri != null) {
applyUriAndSave(uri);
}
return true;
}
return false;
}
public void resetToDefaultAndClear() {
mRootView.setBackgroundResource(R.drawable.list_background);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
sp.edit().remove(PREF_BG_TYPE).remove(PREF_BG_COLOR).remove(PREF_BG_RES_ID).remove(PREF_BG_URI).commit();
}
private void applyColorBackground(int color) {
mRootView.setBackgroundColor(color);
}
private void applyBuiltinBackground(int resId) {
mRootView.setBackgroundResource(resId);
}
private void applyUriBackground(Uri uri) {
try {
InputStream is = mActivity.getContentResolver().openInputStream(uri);
if (is != null) {
Drawable d = Drawable.createFromStream(is, uri.toString());
mRootView.setBackgroundDrawable(d);
is.close();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(mActivity, "无法加载图片", Toast.LENGTH_SHORT).show();
}
}
private void applyUriAndSave(Uri uri) {
try {
InputStream is = mActivity.getContentResolver().openInputStream(uri);
if (is != null) {
Drawable d = Drawable.createFromStream(is, uri.toString());
mRootView.setBackgroundDrawable(d);
is.close();
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
sp.edit().putInt(PREF_BG_TYPE, BG_TYPE_URI).putString(PREF_BG_URI, uri.toString()).commit();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(mActivity, "无法加载图片", Toast.LENGTH_SHORT).show();
}
}
}

@ -1,102 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.util.Log;
import net.micode.notes.data.Notes;
/**
* 广
*/
public class CallRecordReceiver extends BroadcastReceiver {
private static final String TAG = "CallRecordReceiver";
private static String mLastPhoneNumber;// 记录最后一个电话号码
private static long mCallStartTime;// 记录通话开始时间
private static boolean mIsOutgoingCall;// 是否为拨出电话
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() != null) {
Log.d(TAG, "Received broadcast: " + intent.getAction());
if (intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
String phoneNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
Log.d(TAG, "Phone state changed: " + state + ", number: " + phoneNumber);
if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) {
// 电话响铃,记录来电号码
mLastPhoneNumber = phoneNumber;
mCallStartTime = System.currentTimeMillis();
mIsOutgoingCall = false;
Log.d(TAG, "Incoming call ringing: " + phoneNumber);
} else if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(state)) {
// 电话接通,记录开始时间
if (mCallStartTime == 0) {
mCallStartTime = System.currentTimeMillis();
}
if (phoneNumber != null) {
mLastPhoneNumber = phoneNumber;
}
Log.d(TAG, "Call answered");
} else if (TelephonyManager.EXTRA_STATE_IDLE.equals(state)) {
// 电话挂断,创建通话记录便签
if (mLastPhoneNumber != null && mCallStartTime > 0) {
long callEndTime = System.currentTimeMillis();
long callDuration = callEndTime - mCallStartTime;
// 创建通话记录便签
Log.d(TAG, "Call ended, creating call note for: " + mLastPhoneNumber);
Intent serviceIntent = new Intent(context, CallRecordService.class);
serviceIntent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, mLastPhoneNumber);
serviceIntent.putExtra("call_date", callEndTime);
serviceIntent.putExtra("call_duration", callDuration);
// 启动服务创建通话记录便签
try {
context.startService(serviceIntent);
Log.d(TAG, "Successfully started CallRecordService");
} catch (Exception e) {
Log.e(TAG, "Error starting CallRecordService", e);
}
}
// 重置状态
mLastPhoneNumber = null;
mCallStartTime = 0;
mIsOutgoingCall = false;
Log.d(TAG, "Call state reset to idle");
}
} else if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
// 拨出电话
String phoneNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
if (phoneNumber != null) {
mLastPhoneNumber = phoneNumber;
mCallStartTime = System.currentTimeMillis();
mIsOutgoingCall = true;
Log.d(TAG, "Outgoing call started: " + phoneNumber);
}
}
}
}
}

@ -1,105 +0,0 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.IntentService;
import android.content.Intent;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.ResourceParser;
/**
* 便
*/
public class CallRecordService extends IntentService {
private static final String TAG = "CallRecordService";
public CallRecordService() {
super("CallRecordService");
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
String phoneNumber = intent.getStringExtra(android.telephony.TelephonyManager.EXTRA_INCOMING_NUMBER);
long callDate = intent.getLongExtra("call_date", System.currentTimeMillis());
long callDuration = intent.getLongExtra("call_duration", 0);
if (phoneNumber != null) {
Log.d(TAG, "Creating call note for number: " + phoneNumber + ", date: " + callDate);
createCallNote(phoneNumber, callDate, callDuration);
}
}
}
/**
* 便
* @param phoneNumber
* @param callDate
* @param callDuration
*/
private void createCallNote(String phoneNumber, long callDate, long callDuration) {
try {
Log.d(TAG, "Starting createCallNote for number: " + phoneNumber);
// 创建空便签
WorkingNote note = WorkingNote.createEmptyNote(this, Notes.ID_CALL_RECORD_FOLDER,
0, Notes.TYPE_WIDGET_INVALIDE, ResourceParser.BLUE);
// 转换为通话便签
note.convertToCallNote(phoneNumber, callDate);
// 设置空格作为内容,确保能保存
note.setWorkingText(" ");
// 保存便签获取ID
long noteId = -1;
if (note.saveNote()) {
noteId = note.getNoteId();
Log.d(TAG, "Call note created successfully for: " + phoneNumber + ", noteId: " + noteId);
} else {
Log.e(TAG, "Failed to create call note for: " + phoneNumber);
return;
}
// 跳转到刚刚创建的通话记录便签的编辑视图
if (noteId > 0) {
Log.d(TAG, "Attempting to start NoteEditActivity for noteId: " + noteId);
Intent intent = new Intent(this, NoteEditActivity.class);
// 使用 ACTION_VIEW 以便 NoteEditActivity 正确处理并打开已有便签
intent.setAction(Intent.ACTION_VIEW);
// 使用正确的键名传递便签ID
intent.putExtra(Intent.EXTRA_UID, noteId);
intent.putExtra(Notes.INTENT_EXTRA_CALL_DATE, callDate);
intent.putExtra("phone_number", phoneNumber);
// 从服务启动 Activity需要 NEW_TASK 标志;避免清除整个任务栈
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
startActivity(intent);
Log.d(TAG, "Successfully started NoteEditActivity for call note: " + noteId);
} catch (Exception e) {
Log.e(TAG, "Error starting NoteEditActivity", e);
}
}
} catch (Exception e) {
Log.e(TAG, "Error creating call note", e);
}
}
}

@ -0,0 +1,218 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class EncryptedFolderManager {
private static final String TAG = "EncryptedFolderManager";
private final Context mContext;
private final ContentResolver mResolver;
private final Callback mCallback;
public interface Callback {
void onEncryptedFolderCreated();
void onEncryptedFolderUnlocked(NoteItemData data);
}
public EncryptedFolderManager(Context context, ContentResolver resolver, Callback callback) {
mContext = context;
mResolver = resolver;
mCallback = callback;
}
public void showCreateEncryptedFolderDialog() {
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_encrypted_folder, null);
final EditText etName = (EditText) view.findViewById(R.id.et_encrypted_folder_name);
final EditText etQuestion = (EditText) view.findViewById(R.id.et_encrypted_question);
final EditText etAnswer = (EditText) view.findViewById(R.id.et_encrypted_answer);
etAnswer.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
builder.setTitle(R.string.encrypted_folder_title);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, null);
final Dialog dialog = builder.show();
final Button positive = (Button) dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String name = etName.getText().toString().trim();
String question = etQuestion.getText().toString().trim();
String answer = etAnswer.getText().toString().trim();
if (TextUtils.isEmpty(name)) {
etName.setError(mContext.getString(R.string.hint_foler_name));
return;
}
if (TextUtils.isEmpty(question)) {
etQuestion.setError(mContext.getString(R.string.encrypted_question_empty));
return;
}
if (TextUtils.isEmpty(answer)) {
etAnswer.setError(mContext.getString(R.string.encrypted_answer_empty));
return;
}
if (DataUtils.checkVisibleFolderName(mResolver, name)) {
Toast.makeText(mContext,
mContext.getString(R.string.folder_exist, name), Toast.LENGTH_LONG).show();
return;
}
long folderId = createEncryptedFolder(name, question, answer);
if (folderId > 0) {
dialog.dismiss();
if (mCallback != null) {
mCallback.onEncryptedFolderCreated();
}
}
}
});
}
public EncryptedFolderInfo getEncryptedFolderInfo(long folderId) {
Cursor cursor = mResolver.query(Notes.CONTENT_DATA_URI,
new String[] { DataColumns.DATA3, DataColumns.DATA4 },
DataColumns.NOTE_ID + "=? AND " + DataColumns.MIME_TYPE + "=?",
new String[] { String.valueOf(folderId), Notes.DataConstants.ENCRYPTED_FOLDER },
null);
if (cursor == null) {
return null;
}
try {
if (cursor.moveToFirst()) {
String question = cursor.getString(0);
String answerHash = cursor.getString(1);
if (!TextUtils.isEmpty(question) && !TextUtils.isEmpty(answerHash)) {
return new EncryptedFolderInfo(folderId, question, answerHash);
}
}
} finally {
cursor.close();
}
return null;
}
public void showEncryptedUnlockDialog(final EncryptedFolderInfo info, final NoteItemData data) {
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_encrypted_unlock, null);
TextView tvQuestion = (TextView) view.findViewById(R.id.tv_encrypted_question);
final EditText etAnswer = (EditText) view.findViewById(R.id.et_encrypted_answer);
tvQuestion.setText(info.question);
builder.setTitle(R.string.encrypted_unlock_title);
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, null);
final Dialog dialog = builder.show();
final Button positive = (Button) dialog.findViewById(android.R.id.button1);
positive.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String answer = etAnswer.getText().toString().trim();
if (TextUtils.isEmpty(answer)) {
etAnswer.setError(mContext.getString(R.string.encrypted_answer_empty));
return;
}
if (!TextUtils.equals(hashAnswer(answer), info.answerHash)) {
Toast.makeText(mContext, R.string.encrypted_answer_wrong,
Toast.LENGTH_SHORT).show();
return;
}
dialog.dismiss();
if (mCallback != null) {
mCallback.onEncryptedFolderUnlocked(data);
}
}
});
}
private long createEncryptedFolder(String name, String question, String answer) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = mResolver.insert(Notes.CONTENT_NOTE_URI, values);
if (uri == null) {
return -1;
}
long folderId = -1;
try {
folderId = Long.parseLong(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
Log.e(TAG, "Create encrypted folder failed", e);
return -1;
}
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.NOTE_ID, folderId);
dataValues.put(DataColumns.MIME_TYPE, Notes.DataConstants.ENCRYPTED_FOLDER);
dataValues.put(DataColumns.DATA3, question);
dataValues.put(DataColumns.DATA4, hashAnswer(answer));
mResolver.insert(Notes.CONTENT_DATA_URI, dataValues);
return folderId;
}
private String hashAnswer(String answer) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] result = digest.digest(answer.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : result) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "Hash error", e);
return "";
}
}
public static class EncryptedFolderInfo {
private final long folderId;
private final String question;
private final String answerHash;
private EncryptedFolderInfo(long folderId, String question, String answerHash) {
this.folderId = folderId;
this.question = question;
this.answerHash = answerHash;
}
}
}

@ -0,0 +1,172 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import net.micode.notes.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import jp.wasabeef.richeditor.RichEditor;
public class ImageInsertHelper {
private static final String TAG = "ImageInsertHelper";
private final Activity mActivity;
private final int mRequestCode;
public static class Result {
public final boolean success;
public final String localPath;
public final String html;
private Result(boolean success, String localPath, String html) {
this.success = success;
this.localPath = localPath;
this.html = html;
}
}
public ImageInsertHelper(Activity activity, int requestCode) {
mActivity = activity;
mRequestCode = requestCode;
}
public void startPickImage() {
try {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
} else {
intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
intent.setType("image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
}
mActivity.startActivityForResult(intent, mRequestCode);
} catch (ActivityNotFoundException e) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
mActivity.startActivityForResult(intent, mRequestCode);
} catch (ActivityNotFoundException ex) {
Toast.makeText(mActivity, R.string.error_picture_select, Toast.LENGTH_SHORT).show();
Log.e(TAG, "No image picker available", ex);
}
}
}
public Result handleActivityResult(int requestCode, int resultCode, Intent data, RichEditor editor) {
if (requestCode != mRequestCode) {
return null;
}
if (resultCode != Activity.RESULT_OK || data == null) {
return new Result(false, null, null);
}
Uri uri = data.getData();
if (uri == null) {
Toast.makeText(mActivity, R.string.error_picture_select, Toast.LENGTH_SHORT).show();
return new Result(false, null, null);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
final int takeFlags = data.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
try {
mActivity.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException e) {
Log.w(TAG, "Persistable uri permission not granted", e);
}
}
String localImagePath = saveImageToLocal(uri);
if (TextUtils.isEmpty(localImagePath)) {
return new Result(false, null, null);
}
String newHtml = appendImageHtml(editor, localImagePath);
return new Result(true, localImagePath, newHtml);
}
private String appendImageHtml(RichEditor editor, String localImagePath) {
String imgHtmlTag = buildImageHtmlTag(localImagePath);
String curHtml = normalizeEditorHtml(editor.getHtml());
String newHtml = curHtml + imgHtmlTag;
editor.setHtml(newHtml);
editor.focusEditor();
return newHtml;
}
String buildImageHtmlTag(String localImagePath) {
String imgUrl = Uri.fromFile(new File(localImagePath)).toString();
return "<img src=\"" + imgUrl + "\" width=\"200\" height=\"200\"/><br/>";
}
private String normalizeEditorHtml(String html) {
if (TextUtils.isEmpty(html) || "null".equalsIgnoreCase(html)) {
return "";
}
return html;
}
private String saveImageToLocal(Uri uri) {
try {
File baseDir = mActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (baseDir == null) {
baseDir = mActivity.getFilesDir();
}
File appDir = new File(baseDir, "note_images");
if (!appDir.exists() && !appDir.mkdirs()) {
Log.e(TAG, "Create image directory failed: " + appDir.getAbsolutePath());
Toast.makeText(mActivity, R.string.error_picture_select, Toast.LENGTH_SHORT).show();
return null;
}
String fileName = "note_" + System.currentTimeMillis() + ".jpg";
File targetFile = new File(appDir, fileName);
try (InputStream is = mActivity.getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(targetFile)) {
if (is == null) {
Log.e(TAG, "Open image stream failed: " + uri);
Toast.makeText(mActivity, R.string.error_picture_select, Toast.LENGTH_SHORT).show();
return null;
}
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
return targetFile.getAbsolutePath();
} catch (Exception e) {
Log.e(TAG, "Save image failed", e);
Toast.makeText(mActivity, R.string.error_picture_select, Toast.LENGTH_SHORT).show();
return null;
}
}
}

@ -0,0 +1,569 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.AsyncTask;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.HashSet;
public class MemoryBottleDialog extends Dialog implements View.OnClickListener {
private static final String PREF_MEMORY_FOLDER_ID = "pref_memory_bottle_folder_id";
private static final List<MemoryEntry> sAllEntries = new ArrayList<>();
private static final List<MemoryEntry> sRemainingEntries = new ArrayList<>();
private static final Random sRandom = new Random();
private static long sFolderId = Long.MIN_VALUE;
private final Activity mActivity;
private Button mAddButton;
private Button mBrowseButton;
private long mMemoryFolderId = -1;
private boolean mEntriesLoaded;
private boolean mLoading;
private boolean mBrowseLoading;
private LoadTask mLoadTask;
private BrowseTask mBrowseTask;
private PendingAction mPendingAction = PendingAction.NONE;
public MemoryBottleDialog(Activity activity) {
super(activity, android.R.style.Theme_Light_NoTitleBar);
mActivity = activity;
}
@Override
protected void onCreate(android.os.Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.memory_bottle);
getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT);
initResources();
}
@Override
public void dismiss() {
if (mLoadTask != null) {
mLoadTask.cancel(true);
mLoadTask = null;
}
if (mBrowseTask != null) {
mBrowseTask.cancel(true);
mBrowseTask = null;
}
super.dismiss();
}
private void initResources() {
mAddButton = (Button) findViewById(R.id.btn_memory_add);
mBrowseButton = (Button) findViewById(R.id.btn_memory_browse);
mAddButton.setOnClickListener(this);
mBrowseButton.setOnClickListener(this);
updateButtonState();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_memory_add:
requestAction(PendingAction.ADD);
break;
case R.id.btn_memory_browse:
requestAction(PendingAction.BROWSE);
break;
default:
break;
}
}
private void requestAction(PendingAction action) {
if (mLoading || mBrowseLoading) {
Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show();
return;
}
if (mMemoryFolderId <= 0) {
mPendingAction = action;
Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show();
startLoadTask(action == PendingAction.BROWSE);
return;
}
if (action == PendingAction.BROWSE && !mEntriesLoaded) {
mPendingAction = action;
Toast.makeText(mActivity, R.string.memory_bottle_loading, Toast.LENGTH_SHORT).show();
startLoadTask(true);
return;
}
if (action == PendingAction.ADD) {
showAddDialog();
} else if (action == PendingAction.BROWSE) {
browseMemory();
}
}
private void startLoadTask(boolean loadEntries) {
if (mLoadTask != null) {
return;
}
setLoading(true);
mLoadTask = new LoadTask(this, loadEntries);
mLoadTask.execute();
}
private void setLoading(boolean loading) {
mLoading = loading;
updateButtonState();
}
private void setBrowseLoading(boolean loading) {
mBrowseLoading = loading;
updateButtonState();
}
private void updateButtonState() {
boolean enabled = !(mLoading || mBrowseLoading);
if (mAddButton != null) {
mAddButton.setEnabled(enabled);
}
if (mBrowseButton != null) {
mBrowseButton.setEnabled(enabled);
}
}
private void showAddDialog() {
final EditText editText = new EditText(mActivity);
int padding = (int) (mActivity.getResources().getDisplayMetrics().density * 16);
editText.setPadding(padding, padding, padding, padding);
editText.setGravity(Gravity.TOP | Gravity.START);
editText.setMinLines(4);
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
editText.setHint(R.string.memory_bottle_add_hint);
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
builder.setTitle(R.string.memory_bottle_add_title);
builder.setView(editText);
builder.setPositiveButton(android.R.string.ok, null);
builder.setNegativeButton(android.R.string.cancel, null);
final AlertDialog dialog = builder.show();
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String content = editText.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(mActivity, R.string.memory_bottle_empty_input,
Toast.LENGTH_SHORT).show();
return;
}
if (createMemoryNote(content)) {
dialog.dismiss();
}
}
});
}
private boolean createMemoryNote(String content) {
if (mMemoryFolderId <= 0) {
Toast.makeText(mActivity, R.string.memory_bottle_folder_error, Toast.LENGTH_SHORT).show();
return false;
}
WorkingNote note = WorkingNote.createEmptyNote(mActivity, mMemoryFolderId,
AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
ResourceParser.getDefaultBgId(mActivity));
note.setWorkingText(content);
if (!note.saveNote()) {
Toast.makeText(mActivity, R.string.memory_bottle_save_failed, Toast.LENGTH_SHORT).show();
return false;
}
long noteId = note.getNoteId();
long createdDate = queryNoteCreatedDate(noteId);
MemoryEntry entry = new MemoryEntry(noteId, createdDate, content);
sAllEntries.add(entry);
sRemainingEntries.add(entry);
mEntriesLoaded = true;
Toast.makeText(mActivity, R.string.memory_bottle_save_success, Toast.LENGTH_SHORT).show();
return true;
}
private long queryNoteCreatedDate(long noteId) {
Cursor cursor = mActivity.getContentResolver().query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
new String[] { NoteColumns.CREATED_DATE },
null, null, null);
if (cursor == null) {
return System.currentTimeMillis();
}
try {
if (cursor.moveToFirst()) {
return cursor.getLong(0);
}
} finally {
cursor.close();
}
return System.currentTimeMillis();
}
private void browseMemory() {
if (sAllEntries.isEmpty()) {
Toast.makeText(mActivity, R.string.memory_bottle_empty, Toast.LENGTH_SHORT).show();
return;
}
if (sRemainingEntries.isEmpty()) {
showBrowseFinishedDialog();
return;
}
showRandomEntry();
}
private void showBrowseFinishedDialog() {
new AlertDialog.Builder(mActivity)
.setMessage(R.string.memory_bottle_browse_done)
.setPositiveButton(R.string.memory_bottle_restart, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
resetRemainingEntries();
showRandomEntry();
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void resetRemainingEntries() {
sRemainingEntries.clear();
sRemainingEntries.addAll(sAllEntries);
}
private void showRandomEntry() {
int index = sRandom.nextInt(sRemainingEntries.size());
MemoryEntry entry = sRemainingEntries.remove(index);
startBrowseTask(entry);
}
private void startBrowseTask(MemoryEntry entry) {
if (mBrowseTask != null) {
return;
}
setBrowseLoading(true);
mBrowseTask = new BrowseTask(this, entry);
mBrowseTask.execute();
}
private String formatEntryMessage(long createdDate, String content) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
String date = format.format(new Date(createdDate));
return mActivity.getString(R.string.memory_bottle_entry_format, date, content);
}
private long ensureMemoryFolder() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mActivity);
long storedId = sp.getLong(PREF_MEMORY_FOLDER_ID, Long.MIN_VALUE);
if (storedId > 0 && DataUtils.visibleInNoteDatabase(mActivity.getContentResolver(),
storedId, Notes.TYPE_FOLDER)) {
return storedId;
}
String folderName = mActivity.getString(R.string.memory_bottle_folder_name);
long folderId = queryFolderIdByName(folderName);
if (folderId > 0) {
sp.edit().putLong(PREF_MEMORY_FOLDER_ID, folderId).commit();
return folderId;
}
folderId = createMemoryFolder(folderName);
if (folderId > 0) {
sp.edit().putLong(PREF_MEMORY_FOLDER_ID, folderId).commit();
return folderId;
}
return -1;
}
private long queryFolderIdByName(String name) {
ContentResolver resolver = mActivity.getContentResolver();
Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.ID },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?"
+ " AND " + NoteColumns.SNIPPET + "=?",
new String[] { String.valueOf(Notes.TYPE_FOLDER),
String.valueOf(Notes.ID_TRASH_FOLER), name },
null);
if (cursor == null) {
return 0;
}
try {
if (cursor.moveToFirst()) {
return cursor.getLong(0);
}
} finally {
cursor.close();
}
return 0;
}
private long createMemoryFolder(String name) {
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
android.net.Uri uri = mActivity.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
if (uri == null) {
return 0;
}
try {
return Long.parseLong(uri.getPathSegments().get(1));
} catch (NumberFormatException e) {
return 0;
}
}
private String queryNoteContent(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(
Notes.CONTENT_DATA_URI,
new String[] { DataColumns.CONTENT },
DataColumns.NOTE_ID + "=? AND " + DataColumns.MIME_TYPE + "=?",
new String[] { String.valueOf(noteId), TextNote.CONTENT_ITEM_TYPE },
null);
if (cursor == null) {
return "";
}
try {
if (cursor.moveToFirst()) {
return cursor.getString(0);
}
} finally {
cursor.close();
}
return "";
}
private List<MemoryEntry> loadEntriesFromDatabase(long folderId) {
List<MemoryEntry> entries = new ArrayList<>();
ContentResolver resolver = mActivity.getContentResolver();
Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI,
new String[] { NoteColumns.ID, NoteColumns.CREATED_DATE },
NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "=?",
new String[] { String.valueOf(Notes.TYPE_NOTE), String.valueOf(folderId) },
NoteColumns.CREATED_DATE + " DESC");
if (cursor == null) {
return entries;
}
try {
while (cursor.moveToNext()) {
long noteId = cursor.getLong(0);
long createdDate = cursor.getLong(1);
entries.add(new MemoryEntry(noteId, createdDate, ""));
}
} finally {
cursor.close();
}
return entries;
}
private static final class MemoryEntry {
private final long id;
private final long createdDate;
private final String content;
private MemoryEntry(long id, long createdDate, String content) {
this.id = id;
this.createdDate = createdDate;
this.content = content;
}
}
private static final class LoadResult {
private final long folderId;
private final List<MemoryEntry> entries;
private final boolean loadedEntries;
private LoadResult(long folderId, List<MemoryEntry> entries, boolean loadedEntries) {
this.folderId = folderId;
this.entries = entries;
this.loadedEntries = loadedEntries;
}
}
private static final class LoadTask extends AsyncTask<Void, Void, LoadResult> {
private final WeakReference<MemoryBottleDialog> mRef;
private final boolean mLoadEntries;
private LoadTask(MemoryBottleDialog dialog, boolean loadEntries) {
mRef = new WeakReference<>(dialog);
mLoadEntries = loadEntries;
}
@Override
protected LoadResult doInBackground(Void... params) {
MemoryBottleDialog dialog = mRef.get();
if (dialog == null) {
return null;
}
long folderId = dialog.ensureMemoryFolder();
List<MemoryEntry> entries = new ArrayList<>();
if (folderId > 0 && mLoadEntries) {
entries = dialog.loadEntriesFromDatabase(folderId);
}
return new LoadResult(folderId, entries, mLoadEntries);
}
@Override
protected void onPostExecute(LoadResult result) {
MemoryBottleDialog dialog = mRef.get();
if (dialog == null || !dialog.isShowing()) {
return;
}
dialog.mLoadTask = null;
dialog.setLoading(false);
if (result == null || result.folderId <= 0) {
Toast.makeText(dialog.mActivity, R.string.memory_bottle_folder_error,
Toast.LENGTH_SHORT).show();
dialog.mPendingAction = PendingAction.NONE;
return;
}
dialog.mMemoryFolderId = result.folderId;
sFolderId = result.folderId;
if (result.loadedEntries) {
sAllEntries.clear();
sAllEntries.addAll(result.entries);
sRemainingEntries.clear();
sRemainingEntries.addAll(result.entries);
dialog.mEntriesLoaded = true;
} else if (sFolderId == result.folderId) {
dialog.mEntriesLoaded = !sAllEntries.isEmpty();
}
PendingAction pending = dialog.mPendingAction;
dialog.mPendingAction = PendingAction.NONE;
if (pending == PendingAction.ADD) {
dialog.showAddDialog();
} else if (pending == PendingAction.BROWSE) {
dialog.browseMemory();
}
}
}
private static final class BrowseResult {
private final MemoryEntry entry;
private final String content;
private BrowseResult(MemoryEntry entry, String content) {
this.entry = entry;
this.content = content;
}
}
private static final class BrowseTask extends AsyncTask<Void, Void, BrowseResult> {
private final WeakReference<MemoryBottleDialog> mRef;
private final MemoryEntry mEntry;
private BrowseTask(MemoryBottleDialog dialog, MemoryEntry entry) {
mRef = new WeakReference<>(dialog);
mEntry = entry;
}
@Override
protected BrowseResult doInBackground(Void... params) {
MemoryBottleDialog dialog = mRef.get();
if (dialog == null) {
return null;
}
String content = mEntry.content;
if (TextUtils.isEmpty(content)) {
content = dialog.queryNoteContent(dialog.mActivity.getContentResolver(), mEntry.id);
}
if (TextUtils.isEmpty(content)) {
content = dialog.mActivity.getString(R.string.memory_bottle_missing_content);
}
return new BrowseResult(mEntry, content);
}
@Override
protected void onPostExecute(BrowseResult result) {
MemoryBottleDialog dialog = mRef.get();
if (dialog == null || !dialog.isShowing()) {
return;
}
dialog.mBrowseTask = null;
dialog.setBrowseLoading(false);
if (result == null) {
return;
}
String message = dialog.formatEntryMessage(result.entry.createdDate, result.content);
new AlertDialog.Builder(dialog.mActivity)
.setTitle(R.string.memory_bottle_title)
.setMessage(message)
.setPositiveButton(R.string.memory_bottle_close, null)
.setNegativeButton(R.string.memory_bottle_delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
dialog.deleteMemoryEntry(result.entry);
}
})
.show();
}
}
private void deleteMemoryEntry(MemoryEntry entry) {
if (mMemoryFolderId <= 0) {
Toast.makeText(mActivity, R.string.memory_bottle_folder_error, Toast.LENGTH_SHORT).show();
return;
}
HashSet<Long> ids = new HashSet<Long>();
ids.add(entry.id);
if (DataUtils.batchMoveToTrash(mActivity.getContentResolver(), ids, mMemoryFolderId)) {
sAllEntries.remove(entry);
sRemainingEntries.remove(entry);
Toast.makeText(mActivity, R.string.memory_bottle_delete_success, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(mActivity, R.string.memory_bottle_delete_failed, Toast.LENGTH_SHORT).show();
}
}
private enum PendingAction {
NONE,
ADD,
BROWSE
}
}

File diff suppressed because it is too large Load Diff

@ -86,8 +86,6 @@ public class NoteEditText extends EditText {
}
private OnTextViewChangeListener mOnTextViewChangeListener; // 文本变化监听器实例
//新增选区变化回调接口
private OnSelectionChangeListener mOnSelectionChangeListener;
/**
* NoteEditText
@ -113,10 +111,6 @@ public class NoteEditText extends EditText {
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
//新增选区变化回调接口设置方法
public void setOnSelectionChangeListener(OnSelectionChangeListener listener) {
mOnSelectionChangeListener = listener;
}
/**
* XMLNoteEditText
@ -291,19 +285,4 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
//新增选区变化回调接口实现
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (mOnSelectionChangeListener != null) {
mOnSelectionChangeListener.onSelectionChanged(mIndex, selStart, selEnd);
}
}
}
// 新增选区变化回调接口
interface OnSelectionChangeListener {
void onSelectionChanged(int index, int selStart, int selEnd);
}

@ -110,13 +110,8 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
// habit flag, may not exist on older DBs
try {
mIsHabit = cursor.getInt(IS_HABIT_COLUMN) > 0;
} catch (Exception e) {
mIsHabit = false;
}
// 处理通话记录相关属性
mPhoneNumber = "";
if (mParentId == Notes.ID_CALL_RECORD_FOLDER) {
// 获取通话记录的电话号码
@ -331,11 +326,6 @@ public class NoteItemData {
return (mAlertDate > 0);
}
public boolean isHabit() {
return mIsHabit;
}
/**
*
* @return truefalse

File diff suppressed because it is too large Load Diff

@ -31,80 +31,89 @@ import java.util.HashSet;
import java.util.Iterator;
public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和ListView之间的适配器将Cursor数据转换为ListView可以理解的视图格式
private static final String TAG = "NotesListAdapter"; // 日志标签
private Context mContext; // 上下文对象
private HashMap<Integer, Boolean> mSelectedIndex; // 选中项索引映射
private int mNotesCount; // 笔记数量(排除文件夹)
private boolean mChoiceMode; // 是否处于选择模式
public static class AppWidgetAttribute { // 小部件属性类用于存储小部件的ID和类型
public int widgetId; // 小部件ID
public int widgetType; // 小部件类型
public class NotesListAdapter extends CursorAdapter {
private static final String TAG = "NotesListAdapter";
private Context mContext;
private HashMap<Integer, Boolean> mSelectedIndex;
private int mNotesCount;
private boolean mChoiceMode;
private boolean mIncludeFolders;
public static class AppWidgetAttribute {
public int widgetId;
public int widgetType;
};
public NotesListAdapter(Context context) { // 构造函数创建NotesListAdapter实例
public NotesListAdapter(Context context) {
super(context, null);
mSelectedIndex = new HashMap<Integer, Boolean>(); // 初始化选中项映射
mSelectedIndex = new HashMap<Integer, Boolean>();
mContext = context;
mNotesCount = 0;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return new NotesListItem(context); // 创建新的NotesListItem
return new NotesListItem(context);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
if (view instanceof NotesListItem) {
NoteItemData itemData = new NoteItemData(context, cursor); // 创建数据模型
((NotesListItem) view).bind(context, itemData, mChoiceMode, // 绑定数据到视图
NoteItemData itemData = new NoteItemData(context, cursor);
((NotesListItem) view).bind(context, itemData, mChoiceMode,
isSelectedItem(cursor.getPosition()));
}
}
public void setCheckedItem(final int position, final boolean checked) {
mSelectedIndex.put(position, checked); // 更新选中状态
notifyDataSetChanged(); // 通知数据变化刷新UI
mSelectedIndex.put(position, checked);
notifyDataSetChanged();
}
public boolean isInChoiceMode() {
return mChoiceMode; // 检查是否处于选择模式
return mChoiceMode;
}
public void setChoiceMode(boolean mode) {
mSelectedIndex.clear(); // 清除之前的选中状态
mChoiceMode = mode; // 更新模式标志
mSelectedIndex.clear();
mChoiceMode = mode;
mIncludeFolders = false;
}
public void setChoiceMode(boolean mode, boolean includeFolders) {
mSelectedIndex.clear();
mChoiceMode = mode;
mIncludeFolders = includeFolders;
}
public void selectAll(boolean checked) {
Cursor cursor = getCursor();
for (int i = 0; i < getCount(); i++) {
if (cursor.moveToPosition(i)) {
if (NoteItemData.getNoteType(cursor) == Notes.TYPE_NOTE) { // 只处理普通笔记,跳过文件夹
setCheckedItem(i, checked); // 设置选中状态
int type = NoteItemData.getNoteType(cursor);
if (type == Notes.TYPE_NOTE || (mIncludeFolders && type == Notes.TYPE_FOLDER)) {
setCheckedItem(i, checked);
}
}
}
}
public HashSet<Long> getSelectedItemIds() {// 获取选中项的ID集合
public HashSet<Long> getSelectedItemIds() {
HashSet<Long> itemSet = new HashSet<Long>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
Long id = getItemId(position); // 获取选中项的ID
Long id = getItemId(position);
if (id == Notes.ID_ROOT_FOLDER) {
Log.d(TAG, "Wrong item id, should not happen");
} else {
itemSet.add(id); // 添加到集合
itemSet.add(id);
}
}
}
return itemSet;
}
public HashSet<AppWidgetAttribute> getSelectedWidget() {// 获取选中项的小部件属性集合
public HashSet<AppWidgetAttribute> getSelectedWidget() {
HashSet<AppWidgetAttribute> itemSet = new HashSet<AppWidgetAttribute>();
for (Integer position : mSelectedIndex.keySet()) {
if (mSelectedIndex.get(position) == true) {
@ -112,10 +121,12 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L
if (c != null) {
AppWidgetAttribute widget = new AppWidgetAttribute();
NoteItemData item = new NoteItemData(mContext, c);
widget.widgetId = item.getWidgetId(); // 获取小部件ID
widget.widgetType = item.getWidgetType(); // 获取小部件类型
widget.widgetId = item.getWidgetId();
widget.widgetType = item.getWidgetType();
itemSet.add(widget);
// Don't close cursor here, only the adapter could close it
/**
* Don't close cursor here, only the adapter could close it
*/
} else {
Log.e(TAG, "Invalid cursor");
return null;
@ -125,7 +136,7 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L
return itemSet;
}
public int getSelectedCount() {// 获取选中项数量
public int getSelectedCount() {
Collection<Boolean> values = mSelectedIndex.values();
if (null == values) {
return 0;
@ -137,39 +148,39 @@ public class NotesListAdapter extends CursorAdapter {//Adapter作为Activity和L
count++;
}
}
return count; // 返回选中项数量
return count;
}
public boolean isAllSelected() {// 检查是否所有笔记都被选中
public boolean isAllSelected() {
int checkedCount = getSelectedCount();
return (checkedCount != 0 && checkedCount == mNotesCount);
return (checkedCount != 0 && checkedCount == mNotesCount);
}
public boolean isSelectedItem(final int position) {// 检查指定位置的项是否被选中
public boolean isSelectedItem(final int position) {
if (null == mSelectedIndex.get(position)) {
return false;
}
return mSelectedIndex.get(position); // 检查指定位置的项是否被选中
return mSelectedIndex.get(position);
}
@Override
protected void onContentChanged() {
super.onContentChanged();
calcNotesCount(); // 当内容变化时,重新计算笔记数量
calcNotesCount();
}
@Override
public void changeCursor(Cursor cursor) {
super.changeCursor(cursor);
calcNotesCount(); // 当游标变化时,重新计算笔记数量
calcNotesCount();
}
private void calcNotesCount() {// 计算普通笔记的数量
private void calcNotesCount() {
mNotesCount = 0;
for (int i = 0; i < getCount(); i++) {
Cursor c = (Cursor) getItem(i);
if (c != null) {
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) { // 只统计普通笔记,排除文件夹
if (NoteItemData.getNoteType(c) == Notes.TYPE_NOTE) {
mNotesCount++;
}
} else {

@ -31,16 +31,13 @@ import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
public class NotesListItem extends LinearLayout {
private ImageView mAlert; // 提醒图标(时钟或通话记录图标)
private TextView mTitle; // 笔记/文件夹标题或内容摘要
private TextView mTime; // 最后修改时间
private TextView mCallName; // 通话记录来电者名称(仅通话记录显示)
private NoteItemData mItemData; // 当前列表项的数据模型
private CheckBox mCheckBox; // 选择模式下的复选框(用于批量操作)
private ImageView mAlert;
private TextView mTitle;
private TextView mTime;
private TextView mCallName;
private NoteItemData mItemData;
private CheckBox mCheckBox;
/**
*
*/
public NotesListItem(Context context) {
super(context);
inflate(context, R.layout.note_item, this);
@ -51,13 +48,7 @@ public class NotesListItem extends LinearLayout {
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
}
/**
* UI
* @param data
* @param checked
*/
public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) {
// 处理选择模式显示
if (choiceMode && data.getType() == Notes.TYPE_NOTE) {
mCheckBox.setVisibility(View.VISIBLE);
mCheckBox.setChecked(checked);
@ -66,7 +57,6 @@ public class NotesListItem extends LinearLayout {
}
mItemData = data;
// 处理通话记录文件夹
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
@ -74,9 +64,14 @@ public class NotesListItem extends LinearLayout {
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
}
// 处理通话记录笔记
else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
} else if (data.getId() == Notes.ID_TRASH_FOLER) {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
mTitle.setText(context.getString(R.string.trash_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(android.R.drawable.ic_menu_delete);
} else if (data.getParentId() == Notes.ID_CALL_RECORD_FOLDER) {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
@ -87,9 +82,14 @@ public class NotesListItem extends LinearLayout {
} else {
mAlert.setVisibility(View.GONE);
}
}
// 处理普通文件夹和笔记
else {
} 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.trash_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.baseline_restore_from_trash_24);
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
@ -108,20 +108,14 @@ public class NotesListItem extends LinearLayout {
}
}
}
// 设置最后修改时间(相对时间格式)
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
// 根据数据类型和位置设置背景
setBackground(data);
}
/**
*
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
if (data.getType() == Notes.TYPE_NOTE) {
// 根据笔记在列表中的位置设置不同背景
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
} else if (data.isLast()) {
@ -132,14 +126,10 @@ public class NotesListItem extends LinearLayout {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
}
} else {
// 文件夹使用统一背景
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
}
}
/**
*
*/
public NoteItemData getItemData() {
return mItemData;
}

@ -91,9 +91,6 @@ public class NotesPreferenceActivity extends PreferenceActivity {
mReceiver = new GTaskReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(mReceiver, filter);
mOriAccounts = null;

@ -0,0 +1,192 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.HashSet;
public class TrashManager {
private static final String TAG = "TrashManager";
private final Context mContext;
private final ContentResolver mResolver;
private final Callback mCallback;
public interface Callback {
void onWidgetsNeedUpdate(HashSet<AppWidgetAttribute> widgets);
void onListChanged();
void onActionModeFinished();
void onRestoreInvalid();
}
public TrashManager(Context context, ContentResolver resolver, Callback callback) {
mContext = context;
mResolver = resolver;
mCallback = callback;
}
public void cleanupExpiredTrash() {
long expireTime = System.currentTimeMillis() - 24L * 60L * 60L * 1000L;
mResolver.delete(Notes.CONTENT_NOTE_URI,
NoteColumns.PARENT_ID + "=? AND " + NoteColumns.MODIFIED_DATE + "<?",
new String[] { String.valueOf(Notes.ID_TRASH_FOLER), String.valueOf(expireTime) });
}
public void batchDelete(final boolean inTrash, final HashSet<Long> ids,
final HashSet<AppWidgetAttribute> widgets, final long originFolderId) {
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
if (inTrash) {
if (!DataUtils.batchDeleteNotes(mResolver, ids)) {
Log.e(TAG, "Delete notes error, should not happens");
}
} else {
if (!DataUtils.batchMoveToTrash(mResolver, ids, originFolderId)) {
Log.e(TAG, "Move notes to trash folder error");
}
}
return widgets;
}
@Override
protected void onPostExecute(HashSet<AppWidgetAttribute> resultWidgets) {
if (mCallback != null) {
mCallback.onWidgetsNeedUpdate(resultWidgets);
mCallback.onListChanged();
mCallback.onActionModeFinished();
}
}
}.execute();
}
public void restoreSelected(final HashSet<Long> ids, final HashSet<AppWidgetAttribute> widgets) {
new AsyncTask<Void, Void, Boolean>() {
protected Boolean doInBackground(Void... params) {
boolean hasInvalid = false;
long now = System.currentTimeMillis();
for (long id : ids) {
Cursor cursor = mResolver.query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
new String[] { NoteColumns.ORIGIN_PARENT_ID, NoteColumns.TYPE },
null, null, null);
if (cursor == null) {
continue;
}
long originParent = Notes.ID_ROOT_FOLDER;
int type = Notes.TYPE_NOTE;
try {
if (cursor.moveToFirst()) {
originParent = cursor.getLong(0);
type = cursor.getInt(1);
}
} finally {
cursor.close();
}
long targetParent = resolveRestoreParent(originParent);
if (targetParent != originParent) {
hasInvalid = true;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, targetParent);
values.put(NoteColumns.ORIGIN_PARENT_ID, 0);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.MODIFIED_DATE, now);
mResolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
values, null, null);
if (type == Notes.TYPE_FOLDER) {
restoreNotesForFolder(id, now);
}
}
return hasInvalid;
}
@Override
protected void onPostExecute(Boolean hasInvalid) {
if (mCallback != null) {
if (hasInvalid != null && hasInvalid) {
mCallback.onRestoreInvalid();
}
mCallback.onWidgetsNeedUpdate(widgets);
mCallback.onListChanged();
mCallback.onActionModeFinished();
}
}
}.execute();
}
public HashSet<AppWidgetAttribute> moveFolderToTrash(long folderId, long originFolderId) {
HashSet<AppWidgetAttribute> widgets = DataUtils.getFolderNoteWidget(mResolver, folderId);
DataUtils.moveNotesToTrashForFolder(mResolver, folderId);
HashSet<Long> ids = new HashSet<Long>();
ids.add(folderId);
if (!DataUtils.batchMoveToTrash(mResolver, ids, originFolderId)) {
Log.e(TAG, "Move folder to trash error");
}
return widgets;
}
private void restoreNotesForFolder(long folderId, long now) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, folderId);
values.put(NoteColumns.ORIGIN_PARENT_ID, 0);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.MODIFIED_DATE, now);
mResolver.update(Notes.CONTENT_NOTE_URI, values,
NoteColumns.PARENT_ID + "=? AND " + NoteColumns.ORIGIN_PARENT_ID + "=?",
new String[] { String.valueOf(Notes.ID_TRASH_FOLER), String.valueOf(folderId) });
}
private long resolveRestoreParent(long originParentId) {
if (originParentId == Notes.ID_ROOT_FOLDER || originParentId == Notes.ID_CALL_RECORD_FOLDER) {
return originParentId;
}
if (originParentId <= 0) {
return Notes.ID_ROOT_FOLDER;
}
Cursor cursor = mResolver.query(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, originParentId),
new String[] { NoteColumns.ID, NoteColumns.PARENT_ID },
NoteColumns.PARENT_ID + "<>?",
new String[] { String.valueOf(Notes.ID_TRASH_FOLER) },
null);
if (cursor == null) {
return Notes.ID_ROOT_FOLDER;
}
try {
if (cursor.moveToFirst()) {
return originParentId;
}
} finally {
cursor.close();
}
return Notes.ID_ROOT_FOLDER;
}
}
Loading…
Cancel
Save