diff --git a/app/src/main/java/net/micode/notes/tool/BackupUtils.java b/app/src/main/java/net/micode/notes/tool/BackupUtils.java new file mode 100644 index 0000000..8dbec2e --- /dev/null +++ b/app/src/main/java/net/micode/notes/tool/BackupUtils.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.database.Cursor; +import android.os.Environment; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.util.Log; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.DataColumns; +import net.micode.notes.data.Notes.DataConstants; +import net.micode.notes.data.Notes.NoteColumns; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; + + +public class BackupUtils { + private static final String TAG = "BackupUtils"; + // Singleton stuff + private static BackupUtils sInstance; + + public static synchronized BackupUtils getInstance(Context context) { + if (sInstance == null) { + sInstance = new BackupUtils(context); + } + return sInstance; + } +/* `TAG` 是一个 String 常量,用于标识日志输出的 tag。 + `sInstance` 是一个静态变量,用于存储单例实例。 + `getInstance()` 是一个静态方法,通过传入一个 `Context` 参数获取 `BackupUtils` 的单例实例。 + 这里使用了双重检查锁定来确保线程安全。 +*/ + + /** + * Following states are signs to represents backup or restore(以下是表示备份或恢复的符号) + * status + */ + // Currently, the sdcard is not mounted + public static final int STATE_SD_CARD_UNMOUONTED = 0; + // The backup file not exist(备份文件不存在) + public static final int STATE_BACKUP_FILE_NOT_EXIST = 1; + // The data is not well formated, may be changed by other programs(数据格式不正确可能被其他程序更改) + public static final int STATE_DATA_DESTROIED = 2; + // Some run-time exception which causes restore or backup fails(导致备份或还原失败的默写运行异常) + public static final int STATE_SYSTEM_ERROR = 3; + // Backup or restore success(备份或还原成功) + public static final int STATE_SUCCESS = 4; + + private TextExport mTextExport;//实例化对象 + + private BackupUtils(Context context) { + mTextExport = new TextExport(context); + } + + private static boolean externalStorageAvailable() { + return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); + } + + public int exportToText() { + return mTextExport.exportToText(); + }//这是一个 public 方法,用于将数据导出为文本,并返回一个整数值。 + + public String getExportedTextFileName() { + return mTextExport.mFileName; + }//这是一个 public 方法,用于获取导出的文本文件名。 + + public String getExportedTextFileDir() { + return mTextExport.mFileDirectory; + }//这是一个 public 方法,用于获取导出的文本文件所在的目录。 + + private static class TextExport { + private static final String[] NOTE_PROJECTION = { + NoteColumns.ID, + NoteColumns.MODIFIED_DATE, + NoteColumns.SNIPPET, + NoteColumns.TYPE + };//这是一个私有的静态内部类,用于完成数据导出的操作。 + + + private static final int NOTE_COLUMN_ID = 0; + + private static final int NOTE_COLUMN_MODIFIED_DATE = 1; + + private static final int NOTE_COLUMN_SNIPPET = 2; + + private static final String[] DATA_PROJECTION = { + DataColumns.CONTENT, + DataColumns.MIME_TYPE, + DataColumns.DATA1, + DataColumns.DATA2, + DataColumns.DATA3, + DataColumns.DATA4, + }; + + private static final int DATA_COLUMN_CONTENT = 0; + + private static final int DATA_COLUMN_MIME_TYPE = 1; + + private static final int DATA_COLUMN_CALL_DATE = 2; + + private static final int DATA_COLUMN_PHONE_NUMBER = 4; + // 定义了多个常量,用于操作笔记和数据的列索引 + + private final String [] TEXT_FORMAT; + private static final int FORMAT_FOLDER_NAME = 0; + private static final int FORMAT_NOTE_DATE = 1; + private static final int FORMAT_NOTE_CONTENT = 2; + // 定义了一个字符串数组 TEXT_FORMAT,其中存储了导出笔记时的文件格式 + + private Context mContext; + private String mFileName; + private String mFileDirectory; + // 定义了 Context mContext、String mFileName、String mFileDirectory 三个变量 + // mContext 存储了当前上下文,mFileName 存储了文件名,mFileDirectory 存储了文件夹路径 + + public TextExport(Context context) { + TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note); + mContext = context; + mFileName = ""; + mFileDirectory = "";// 构造函数,初始化了 TEXT_FORMAT、mContext、mFileName 和 mFileDirectory + } + + private String getFormat(int id) { + return TEXT_FORMAT[id]; + }// getFormat 方法返回 TEXT_FORMAT 数组中指定 id 的字符串 + + /** + * Export the folder identified by folder id to text + * (exportFolderToText 方法用于将指定文件夹下的笔记导出到文本中) + */ + private void exportFolderToText(String folderId, PrintStream ps) { + // Query notes belong to this folder// 查询属于该文件夹的笔记 + Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { + folderId + }, null); + + if (notesCursor != null) { + if (notesCursor.moveToFirst()) { + do { + // Print note's last modified date // 输出笔记的最后修改日期 + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + notesCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note // 查询属于该笔记的数据 + String noteId = notesCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (notesCursor.moveToNext()); + } + notesCursor.close(); + } + } + + /** + * Export note identified by id to a print stream + */ + private void exportNoteToText(String noteId, PrintStream ps) { + Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI, + DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { + noteId + }, null);// exportNoteToText 方法用于将指定笔记的数据导出到文本中 + + if (dataCursor != null) { + if (dataCursor.moveToFirst()) { + do { + String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE); + if (DataConstants.CALL_NOTE.equals(mimeType)) { + // Print phone number // 输出电话号码 + String phoneNumber = dataCursor.getString(DATA_COLUMN_PHONE_NUMBER); + long callDate = dataCursor.getLong(DATA_COLUMN_CALL_DATE); + String location = dataCursor.getString(DATA_COLUMN_CONTENT); + + if (!TextUtils.isEmpty(phoneNumber)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + phoneNumber)); + } + // Print call date// 输出通话日期 + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), DateFormat + .format(mContext.getString(R.string.format_datetime_mdhm), + callDate))); + // Print call attachment location // 输出通话附件位置 + if (!TextUtils.isEmpty(location)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + location)); + } + } else if (DataConstants.NOTE.equals(mimeType)) { + String content = dataCursor.getString(DATA_COLUMN_CONTENT); + if (!TextUtils.isEmpty(content)) { + ps.println(String.format(getFormat(FORMAT_NOTE_CONTENT), + content)); + } + } + } while (dataCursor.moveToNext()); + } + dataCursor.close(); + } + // print a line separator between note// 在导出每个笔记后输出一个分隔符 + try { + ps.write(new byte[] { + Character.LINE_SEPARATOR, Character.LETTER_NUMBER + }); + } catch (IOException e) { + Log.e(TAG, e.toString()); + } + } + + /** + * Note will be exported as text which is user readable + */ + public int exportToText() { + if (!externalStorageAvailable()) { + Log.d(TAG, "Media was not mounted"); + return STATE_SD_CARD_UNMOUONTED; + }/*这段代码的作用是向指定的PrintStream对象中写入一个分隔符。它使用了Java中的byte数组,其中包含了两个特殊的字符:Character.LINE_SEPARATOR和Character.LETTER_NUMBER。 + + Character.LINE_SEPARATOR表示平台的行分隔符,其值因平台而异。例如,在Windows中,它的值是"\r\n",在Unix/Linux中,它的值是"\n"。 + + Character.LETTER_NUMBER是一个没有实际意义的字符,它只是被用来作为分隔符的一部分。 + + 当这个byte数组被写入PrintStream对象时,它会在文本中插入一个分隔符,以便在文本中区分不同的笔记。如果写入过程中发生IOException,那么会在Logcat中输出相应的错误信息。*/ + + PrintStream ps = getExportToTextPrintStream(); + if (ps == null) { + Log.e(TAG, "get print stream error"); + return STATE_SYSTEM_ERROR; + }//这段代码中的 getExportToTextPrintStream() 是一个自定义方法,它返回一个 PrintStream 对象,该对象用于将笔记数据导出到文本文件中 + // First export folder and its notes//“首先导出文件夹及其包含的笔记” + Cursor folderCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR " + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER, null, null); + + if (folderCursor != null) { + if (folderCursor.moveToFirst()) { + do { + // Print folder's name + String folderName = ""; + if(folderCursor.getLong(NOTE_COLUMN_ID) == Notes.ID_CALL_RECORD_FOLDER) { + folderName = mContext.getString(R.string.call_record_folder_name); + } else { + folderName = folderCursor.getString(NOTE_COLUMN_SNIPPET); + } + if (!TextUtils.isEmpty(folderName)) { + ps.println(String.format(getFormat(FORMAT_FOLDER_NAME), folderName)); + } + String folderId = folderCursor.getString(NOTE_COLUMN_ID); + exportFolderToText(folderId, ps); + } while (folderCursor.moveToNext()); + } + folderCursor.close(); + }/*这段代码的作用是导出所有文件夹及其包含的笔记。 + + 首先,通过调用 ContentResolver 的 query() 方法查询所有的文件夹和 Call Record 文件夹,同时排除回收站中的笔记,将结果保存在 Cursor 对象 folderCursor 中。 + + 接着,通过遍历 folderCursor 中的所有记录,获取每个文件夹的名称和ID,并将其写入到输出流 ps 中。如果当前文件夹是 Call Record 文件夹,则使用字符串资源文件中的值作为文件夹的名称*/ + + // Export notes in root's folder + Cursor noteCursor = mContext.getContentResolver().query( + Notes.CONTENT_NOTE_URI, + NOTE_PROJECTION, + NoteColumns.TYPE + "=" + +Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + + "=0", null, null); + + if (noteCursor != null) { + if (noteCursor.moveToFirst()) { + do { + ps.println(String.format(getFormat(FORMAT_NOTE_DATE), DateFormat.format( + mContext.getString(R.string.format_datetime_mdhm), + noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE)))); + // Query data belong to this note + String noteId = noteCursor.getString(NOTE_COLUMN_ID); + exportNoteToText(noteId, ps); + } while (noteCursor.moveToNext()); + } + noteCursor.close(); + } + ps.close(); + + return STATE_SUCCESS; + }/*这段代码的作用是导出根文件夹中的所有笔记。 + + 首先,通过调用 ContentResolver 的 query() 方法查询根文件夹中的所有笔记,并将结果保存在 Cursor 对象 noteCursor 中。 + + 接着,通过遍历 noteCursor 中的所有记录,获取每个笔记的修改日期,并将其写入到输出流 ps 中。然后,调用 exportNoteToText() 方法,将当前笔记的内容导出到输出流 ps 中。 + + 最后,关闭输出流 ps,并返回 STATE_SUCCESS 表示导出笔记数据成功。如果 noteCursor 为空,则不会做任何处理,直接关闭输出流并返回成功状态。*/ + + /** + * Get a print stream pointed to the file {@generateExportedTextFile} + */ + private PrintStream getExportToTextPrintStream() { + File file = generateFileMountedOnSDcard(mContext, R.string.file_path, + R.string.file_name_txt_format); + if (file == null) { + Log.e(TAG, "create file to exported failed"); + return null; + } + mFileName = file.getName(); + mFileDirectory = mContext.getString(R.string.file_path); + PrintStream ps = null; + try { + FileOutputStream fos = new FileOutputStream(file); + ps = new PrintStream(fos); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (NullPointerException e) { + e.printStackTrace(); + return null; + } + return ps; + } + }/*这段代码的作用是创建一个输出流 PrintStream 对象,用于将笔记数据导出到文本文件中。 + +首先,通过调用 generateFileMountedOnSDcard() 方法获取导出文件的路径和名称,并将结果保存在 File 对象 file 中。 + +接着,检查 file 是否为 null。如果是,则在Logcat中输出 "create file to exported failed" 的错误信息,并返回 null。 + +然后,获取 file 的名称和路径,并创建一个 FileOutputStream 对象 fos,将其作为参数传递给 PrintStream 构造函数,创建一个 PrintStream 对象 ps。 + +最后,返回 ps 对象,如果在创建 PrintStream 对象时出现 FileNotFoundException 或 NullPointerException 异常,则返回 null*/ + + /** + * Generate the text file to store imported data + */ + private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) { + StringBuilder sb = new StringBuilder(); + sb.append(Environment.getExternalStorageDirectory()); + sb.append(context.getString(filePathResId)); + File filedir = new File(sb.toString()); + sb.append(context.getString( + fileNameFormatResId, + DateFormat.format(context.getString(R.string.format_date_ymd), + System.currentTimeMillis()))); + File file = new File(sb.toString()); + + try { + if (!filedir.exists()) { + filedir.mkdir(); + } + if (!file.exists()) { + file.createNewFile(); + } + return file; + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } +}/*这段代码的作用是生成一个文件对象,并返回该对象的引用。 + +首先,获取外部存储设备的根目录,并将其与 filePathResId 参数所指定的路径拼接成一个完整的路径,并将其保存在 StringBuilder 对象 sb 中。 + +接着,创建一个 File 对象 filedir,用于表示存储导出文件的目录。如果该目录不存在,则调用 mkdir() 方法创建该目录。 + +然后,使用 fileNameFormatResId 参数所指定的文件名格式,将当前日期和时间添加到 sb 中,形成完整的文件路径。最后,创建一个 File 对象 file,用于表示导出的文件。 + +最后,检查 file 和 filedir 是否存在。如果它们都存在,则直接返回 file 对象的引用。否则,通过捕捉 SecurityException 和 IOException 异常,输出异常信息,并返回 null。*/ + + diff --git a/app/src/main/java/net/micode/notes/tool/DataUtils.java b/app/src/main/java/net/micode/notes/tool/DataUtils.java new file mode 100644 index 0000000..5ba9df3 --- /dev/null +++ b/app/src/main/java/net/micode/notes/tool/DataUtils.java @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.os.RemoteException; +import android.util.Log; + +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.CallNote; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute; + +import java.util.ArrayList; +import java.util.HashSet; + + +public class DataUtils { + public static final String TAG = "DataUtils";//定义了一个Java常量变量,名为TAG + public static boolean batchDeleteNotes(ContentResolver resolver, HashSet ids) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + if (ids.size() == 0) { + Log.d(TAG, "no id is in the hashset"); + return true; + } + + ArrayList operationList = new ArrayList(); + 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)); + operationList.add(builder.build()); + } + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + 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; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个Long类型的HashSet集合。这个方法的作用是批量删除笔记。 + +如果传入的ids集合为空,方法会输出一条日志并返回true;如果ids集合的大小为0,同样会输出一条日志并返回true。 + +如果ids集合不为空且大小不为0,方法会遍历ids集合中的每个元素。如果该元素等于Notes.ID_ROOT_FOLDER,也就是系统文件夹的根目录,那么会输出一个错误日志并跳过该元素;否则,会创建一个ContentProviderOperation对象,使用ContentProviderOperation.newDelete()方法来构建一个删除操作,并将该操作添加到操作列表(operationList)中。 + +最后,方法会使用ContentResolver.applyBatch()方法来执行操作列表中的所有操作,如果操作成功,则返回true,否则返回false。如果发生RemoteException或OperationApplicationException异常,方法会输出一个错误日志并返回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); + resolver.update(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), values, null, null); + }/*这段代码是一个静态方法,它接受四个参数:ContentResolver对象、一个long类型的id、一个long类型的srcFolderId和一个long类型的desFolderId。这个方法的作用是将一个笔记移动到指定的文件夹中。 + +方法首先创建一个ContentValues对象,并将要更新的字段和值添加到该对象中。这里,将NoteColumns.PARENT_ID字段设置为desFolderId,表示将笔记的父文件夹设置为目标文件夹;将NoteColumns.ORIGIN_PARENT_ID字段设置为srcFolderId,表示将笔记的原始父文件夹设置为源文件夹;将NoteColumns.LOCAL_MODIFIED字段设置为1,表示该笔记已被本地修改过。 + +接下来,方法调用ContentResolver.update()方法来更新笔记。该方法接受四个参数:笔记的URI、要更新的值、选择条件和选择条件的参数。这里,笔记的URI是通过ContentUris.withAppendedId()方法创建的,其值为Notes.CONTENT_NOTE_URI与id拼接而成;要更新的值是上面创建的ContentValues对象;选择条件和选择条件的参数都为null,表示更新所有行。*/ + + public static boolean batchMoveToFolder(ContentResolver resolver, HashSet ids, + long folderId) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + + ArrayList operationList = new ArrayList(); + for (long id : ids) { + ContentProviderOperation.Builder builder = ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id)); + builder.withValue(NoteColumns.PARENT_ID, folderId); + builder.withValue(NoteColumns.LOCAL_MODIFIED, 1); + operationList.add(builder.build()); + } + + try { + ContentProviderResult[] results = resolver.applyBatch(Notes.AUTHORITY, operationList); + if (results == null || results.length == 0 || results[0] == null) { + Log.d(TAG, "delete notes failed, ids:" + ids.toString()); + return false; + } + 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; + }/*这段代码是一个静态方法,它接受三个参数:ContentResolver对象、一个Long类型的HashSet集合ids和一个Long类型的folderId。这个方法的作用是批量将笔记移动到指定的文件夹中。 + +如果传入的ids集合为空,方法会输出一条日志并返回true。 + +如果ids集合不为空,方法会遍历ids集合中的每个元素。对于每个元素,方法会创建一个ContentProviderOperation对象,并使用ContentProviderOperation.newUpdate()方法来构建一个更新操作。然后,将NoteColumns.PARENT_ID字段设置为folderId,表示将笔记的父文件夹设置为目标文件夹;将NoteColumns.LOCAL_MODIFIED字段设置为1,表示该笔记已被本地修改过。最后,将该操作添加到操作列表(operationList)中。 + +最后,方法使用ContentResolver.applyBatch()方法来执行操作列表中的所有操作。如果操作成功,则返回true,否则返回false。如果发生RemoteException或OperationApplicationException异常,方法会输出一个错误日志并返回false*/ + + /** + * Get the all folder count except system folders {@link Notes#TYPE_SYSTEM}} + */ + public static int getUserFolderCount(ContentResolver resolver) { + Cursor cursor =resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { "COUNT(*)" }, + NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>?", + new String[] { String.valueOf(Notes.TYPE_FOLDER), String.valueOf(Notes.ID_TRASH_FOLER)}, + null); + + int count = 0; + if(cursor != null) { + if(cursor.moveToFirst()) { + try { + count = cursor.getInt(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "get folder count failed:" + e.toString()); + } finally { + cursor.close(); + } + } + } + return count; + }/*这段代码是一个静态方法,它接受一个ContentResolver对象作为参数。该方法的作用是返回用户创建的文件夹的数量。 + +该方法创建了一个Cursor对象,使用ContentResolver.query()方法查询笔记数据库,并返回所有类型为Notes.TYPE_FOLDER(文件夹类型)且不在回收站中的笔记数量。查询结果只包含一列,即COUNT(*)。 + +查询结果存储在Cursor对象中。如果Cursor对象不为null,方法会将光标移动到第一行,并使用getInt(0)方法获取查询结果中的第一列的值,即文件夹数量。如果发生IndexOutOfBoundsException异常,则输出一个错误日志。最后,方法关闭Cursor对象并返回文件夹数量。 + +注意,该方法只返回用户创建的文件夹数量,不包括系统预置的文件夹(如回收站)。*/ + + 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, + new String [] {String.valueOf(type)}, + null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + }/*这段代码是一个静态方法,它接受三个参数:ContentResolver对象、一个long类型的noteId和一个int类型的type。该方法的作用是检查给定的笔记是否存在于笔记数据库中,并且不在回收站中。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询笔记数据库,查询的条件是笔记类型为type,并且笔记的父文件夹不是回收站。查询结果包含所有列。 + +然后,方法检查查询结果是否存在,如果存在则将exist变量设置为true。最后,方法关闭Cursor对象并返回exist变量的值,即给定的笔记是否存在于笔记数据库中。*/ + + public static boolean existInNoteDatabase(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个long类型的noteId。该方法的作用是检查给定的笔记是否存在于笔记数据库中。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询笔记数据库。查询条件为笔记ID等于noteId,查询结果包含所有列。 + +然后,方法检查查询结果是否存在,如果存在则将exist变量设置为true。最后,方法关闭Cursor对象并返回exist变量的值,即给定的笔记是否存在于笔记数据库中*/ + + public static boolean existInDataDatabase(ContentResolver resolver, long dataId) { + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), + null, null, null, null); + + boolean exist = false; + if (cursor != null) { + if (cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个long类型的dataId。该方法的作用是检查给定的数据是否存在于数据数据库中。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询数据数据库。查询条件为数据ID等于dataId,查询结果包含所有列。 + +然后,方法检查查询结果是否存在,如果存在则将exist变量设置为true。最后,方法关闭Cursor对象并返回exist变量的值,即给定的数据是否存在于数据数据库中。*/ + + 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 + "=?", + new String[] { name }, null); + boolean exist = false; + if(cursor != null) { + if(cursor.getCount() > 0) { + exist = true; + } + cursor.close(); + } + return exist; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个String类型的name。该方法的作用是检查给定名称的文件夹在笔记数据库中是否可见(即不在回收站中)。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询笔记数据库。查询条件为笔记类型为Notes.TYPE_FOLDER(文件夹类型)、父文件夹不是回收站(NoteColumns.PARENT_ID <> Notes.ID_TRASH_FOLER)以及笔记的摘要(NoteColumns.SNIPPET)等于给定的名称。查询结果包含所有列。 + +然后,方法检查查询结果是否存在,如果存在则将exist变量设置为true。最后,方法关闭Cursor对象并返回exist变量的值,即给定名称的文件夹在笔记数据库中是否可见。*/ + + public static HashSet getFolderNoteWidget(ContentResolver resolver, long folderId) { + Cursor c = resolver.query(Notes.CONTENT_NOTE_URI, + new String[] { NoteColumns.WIDGET_ID, NoteColumns.WIDGET_TYPE }, + NoteColumns.PARENT_ID + "=?", + new String[] { String.valueOf(folderId) }, + null); + + HashSet set = null; + if (c != null) { + if (c.moveToFirst()) { + set = new HashSet(); + do { + try { + AppWidgetAttribute widget = new AppWidgetAttribute(); + widget.widgetId = c.getInt(0); + widget.widgetType = c.getInt(1); + set.add(widget); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, e.toString()); + } + } while (c.moveToNext()); + } + c.close(); + } + return set; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个long类型的folderId。该方法的作用是获取给定文件夹下所有包含小部件的笔记的小部件ID和小部件类型。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询笔记数据库。查询条件为笔记的父文件夹ID等于给定的folderId。查询结果包含小部件ID和小部件类型这两列。 + +然后,方法将查询结果装入一个HashSet对象中。如果查询结果不为空,则创建一个HashSet对象,遍历查询结果并逐个添加小部件ID和小部件类型到HashSet中。如果查询结果为空,则返回null。 + +最后,方法关闭Cursor对象并返回HashSet对象,其中包含给定文件夹下所有包含小部件的笔记的小部件ID和小部件类型。*/ + + public static String getCallNumberByNoteId(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.PHONE_NUMBER }, + CallNote.NOTE_ID + "=? AND " + CallNote.MIME_TYPE + "=?", + new String [] { String.valueOf(noteId), CallNote.CONTENT_ITEM_TYPE }, + null); + + if (cursor != null && cursor.moveToFirst()) { + try { + return cursor.getString(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call number fails " + e.toString()); + } finally { + cursor.close(); + } + } + return ""; + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个long类型的noteId。该方法的作用是获取给定笔记ID对应的电话笔记的电话号码。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询数据数据库。查询条件为电话笔记的笔记ID等于给定的noteId,且电话笔记的MIME类型为CallNote.CONTENT_ITEM_TYPE。查询结果包含电话号码这一列。 + +然后,方法检查查询结果是否存在,如果存在则返回查询结果中的电话号码。如果查询结果不存在,则返回空字符串。 + +最后,方法关闭Cursor对象并返回电话号码(或空字符串)。*/ + + public static long getNoteIdByPhoneNumberAndCallDate(ContentResolver resolver, String phoneNumber, long callDate) { + Cursor cursor = resolver.query(Notes.CONTENT_DATA_URI, + new String [] { CallNote.NOTE_ID }, + CallNote.CALL_DATE + "=? AND " + CallNote.MIME_TYPE + "=? AND PHONE_NUMBERS_EQUAL(" + + CallNote.PHONE_NUMBER + ",?)", + new String [] { String.valueOf(callDate), CallNote.CONTENT_ITEM_TYPE, phoneNumber }, + null); + + if (cursor != null) { + if (cursor.moveToFirst()) { + try { + return cursor.getLong(0); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "Get call note id fails " + e.toString()); + } + } + cursor.close(); + } + return 0; + }/*这段代码是一个静态方法,它接受三个参数:ContentResolver对象、一个String类型的phoneNumber和一个long类型的callDate。该方法的作用是根据给定的电话号码和通话日期获取对应的电话笔记的笔记ID。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询数据数据库。查询条件为电话笔记的通话日期等于给定的callDate,电话笔记的MIME类型为CallNote.CONTENT_ITEM_TYPE,且电话号码等于给定的phoneNumber。查询结果包含笔记ID这一列。 + +然后,方法检查查询结果是否存在。如果存在,则返回查询结果中的笔记ID。如果查询结果不存在,则返回0。 + +最后,方法关闭Cursor对象并返回笔记ID(或0)。*/ + + public static String getSnippetById(ContentResolver resolver, long noteId) { + Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI, + new String [] { NoteColumns.SNIPPET }, + NoteColumns.ID + "=?", + new String [] { String.valueOf(noteId)}, + null); + + if (cursor != null) { + String snippet = ""; + if (cursor.moveToFirst()) { + snippet = cursor.getString(0); + } + cursor.close(); + return snippet; + } + throw new IllegalArgumentException("Note is not found with id: " + noteId); + }/*这段代码是一个静态方法,它接受两个参数:ContentResolver对象和一个long类型的noteId。该方法的作用是获取给定笔记ID对应的笔记的摘要(snippet)。 + +该方法首先创建一个Cursor对象,使用ContentResolver.query()方法查询笔记的内容提供者。查询条件为笔记的ID等于给定的noteId。查询结果包含笔记的摘要这一列。 + +然后,方法检查查询结果是否存在。如果存在,则获取查询结果中的笔记摘要并将其存储在一个字符串变量snippet中。然后,方法关闭Cursor对象并返回snippet。如果查询结果不存在,则抛出一个IllegalArgumentException异常,其中包含“Note is not found with id: ”和给定的noteId作为错误消息。*/ + + public static String getFormattedSnippet(String snippet) { + if (snippet != null) { + snippet = snippet.trim(); + int index = snippet.indexOf('\n'); + if (index != -1) { + snippet = snippet.substring(0, index); + } + } + return snippet; + } +}/*这段代码是一个静态方法,它接受一个String类型的snippet参数。该方法的作用是对给定的snippet进行格式化处理,以便在界面上显示。 + +该方法首先检查传入的snippet是否为空。如果不为空,则使用String.trim()方法删除snippet字符串中的前导和尾随空格。然后,使用String.indexOf()方法查找snippet字符串中第一个换行符的位置。如果找到了换行符,则使用String.substring()方法截取snippet字符串中第一个换行符之前的所有字符,并将结果存储回snippet变量中。否则,不做任何修改,直接返回原始的snippet字符串。 + +最后,方法返回格式化后的snippet字符串。*/ diff --git a/app/src/main/java/net/micode/notes/tool/GTaskStringUtils.java b/app/src/main/java/net/micode/notes/tool/GTaskStringUtils.java new file mode 100644 index 0000000..f8ba895 --- /dev/null +++ b/app/src/main/java/net/micode/notes/tool/GTaskStringUtils.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +public class GTaskStringUtils { + + public final static String GTASK_JSON_ACTION_ID = "action_id";// 行动 ID + + public final static String GTASK_JSON_ACTION_LIST = "action_list";// 行动清单 + + public final static String GTASK_JSON_ACTION_TYPE = "action_type";// 行动类型 + + public final static String GTASK_JSON_ACTION_TYPE_CREATE = "create";// 创建行动 + + public final static String GTASK_JSON_ACTION_TYPE_GETALL = "get_all";// 获取全部行动 + + public final static String GTASK_JSON_ACTION_TYPE_MOVE = "move";// 移动行动 + + public final static String GTASK_JSON_ACTION_TYPE_UPDATE = "update"; // 更新行动 + + public final static String GTASK_JSON_CREATOR_ID = "creator_id";// 创建者 ID + + public final static String GTASK_JSON_CHILD_ENTITY = "child_entity";// 子实体 + + public final static String GTASK_JSON_CLIENT_VERSION = "client_version";// 客户端版本 + + public final static String GTASK_JSON_COMPLETED = "completed"; // 完成状态 + + public final static String GTASK_JSON_CURRENT_LIST_ID = "current_list_id";// 当前清单 ID + + public final static String GTASK_JSON_DEFAULT_LIST_ID = "default_list_id"; // 默认清单 ID + + public final static String GTASK_JSON_DELETED = "deleted";// 删除状态 + + public final static String GTASK_JSON_DEST_LIST = "dest_list";// 目标清单 + + public final static String GTASK_JSON_DEST_PARENT = "dest_parent";// 目标父元素 + + public final static String GTASK_JSON_DEST_PARENT_TYPE = "dest_parent_type"; // 目标父元素类型 + + public final static String GTASK_JSON_ENTITY_DELTA = "entity_delta";// 实体增量 + + public final static String GTASK_JSON_ENTITY_TYPE = "entity_type"; // 实体类型 + + public final static String GTASK_JSON_GET_DELETED = "get_deleted"; // 获取删除状态 + + public final static String GTASK_JSON_ID = "id";// ID + + public final static String GTASK_JSON_INDEX = "index";// 索引 + + public final static String GTASK_JSON_LAST_MODIFIED = "last_modified"; // 最后修改时间 + + public final static String GTASK_JSON_LATEST_SYNC_POINT = "latest_sync_point";// 最新同步点 + + public final static String GTASK_JSON_LIST_ID = "list_id"; // 清单 ID + + public final static String GTASK_JSON_LISTS = "lists";// 清单列表 + public final static String GTASK_JSON_NAME = "name";// 名称 + + public final static String GTASK_JSON_NEW_ID = "new_id";// 新 ID + + public final static String GTASK_JSON_NOTES = "notes";// 备注 + + public final static String GTASK_JSON_PARENT_ID = "parent_id";// 父 ID + + public final static String GTASK_JSON_PRIOR_SIBLING_ID = "prior_sibling_id";// 上一个同级 ID + + public final static String GTASK_JSON_RESULTS = "results";// 结果 + + public final static String GTASK_JSON_SOURCE_LIST = "source_list";// 源清单 + + public final static String GTASK_JSON_TASKS = "tasks";// 任务列表 + + public final static String GTASK_JSON_TYPE = "type";// 类型 + + public final static String GTASK_JSON_TYPE_GROUP = "GROUP";// 分组类型 + + public final static String GTASK_JSON_TYPE_TASK = "TASK";// 任务类型 + + public final static String GTASK_JSON_USER = "user";// 用户 + + public final static String MIUI_FOLDER_PREFFIX = "[MIUI_Notes]"; // MIUI 笔记前缀 + + public final static String FOLDER_DEFAULT = "Default";// 默认文件夹 + + public final static String FOLDER_CALL_NOTE = "Call_Note"; // 通话笔记文件夹 + + public final static String FOLDER_META = "METADATA";// 元数据文件夹 + + public final static String META_HEAD_GTASK_ID = "meta_gid";// GTASK ID 元数据头 + + public final static String META_HEAD_NOTE = "meta_note";// 笔记元数据头 + + public final static String META_HEAD_DATA = "meta_data";// 数据元数据头 + + public final static String META_NOTE_NAME = "[META INFO] DON'T UPDATE AND DELETE";// 元数据笔记名称 + +} diff --git a/app/src/main/java/net/micode/notes/tool/ResourceParser.java b/app/src/main/java/net/micode/notes/tool/ResourceParser.java new file mode 100644 index 0000000..fd1e936 --- /dev/null +++ b/app/src/main/java/net/micode/notes/tool/ResourceParser.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.micode.notes.tool; + +import android.content.Context; +import android.preference.PreferenceManager; + +import net.micode.notes.R; +import net.micode.notes.ui.NotesPreferenceActivity; + +public class ResourceParser { + + public static final int YELLOW = 0;// 黄色 + public static final int BLUE = 1;// 蓝色 + public static final int WHITE = 2;// 白色 + public static final int GREEN = 3;// 绿色 + public static final int RED = 4;// 红色 + public static final int BG_DEFAULT_COLOR = YELLOW;// 默认背景颜色 + + + public static final int TEXT_SMALL = 0;// 小号字体 + public static final int TEXT_MEDIUM = 1;// 中号字体 + public static final int TEXT_LARGE = 2;// 大号字体 + public static final int TEXT_SUPER = 3;// 超大号字 + + public static final int BG_DEFAULT_FONT_SIZE = TEXT_MEDIUM;// 默认背景字体大小 + + public static class NoteBgResources { + private final static int [] BG_EDIT_RESOURCES = new int [] { + R.drawable.edit_yellow, + R.drawable.edit_blue, + R.drawable.edit_white, + R.drawable.edit_green, + R.drawable.edit_red + };/*这是一个静态内部类 NoteBgResources,其中包含一个名为 BG_EDIT_RESOURCES 的静态常量数组,该数组包含了 5 个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.edit_yellow、R.drawable.edit_blue、R.drawable.edit_white、R.drawable.edit_green、R.drawable.edit_red。*/ + + private final static int [] BG_EDIT_TITLE_RESOURCES = new int [] { + R.drawable.edit_title_yellow, + R.drawable.edit_title_blue, + R.drawable.edit_title_white, + R.drawable.edit_title_green, + R.drawable.edit_title_red + };/*这是一个私有的静态常量数组 BG_EDIT_TITLE_RESOURCES,它包含了五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.edit_title_yellow、R.drawable.edit_title_blue、R.drawable.edit_title_white、R.drawable.edit_title_green、R.drawable.edit_title_red。*/ + + public static int getNoteBgResource(int id) { + return BG_EDIT_RESOURCES[id]; + }/*一个静态方法 getNoteBgResource(),它接受一个整型参数 id,并返回一个整型值。在这个方法中,静态常量数组 BG_EDIT_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。*/ + + public static int getNoteTitleBgResource(int id) { + return BG_EDIT_TITLE_RESOURCES[id]; + } + }/*一个静态方法 getNoteTitleBgResource(),它接受一个整型参数 id,并返回一个整型值。在这个方法中,静态常量数组 BG_EDIT_TITLE_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。*/ + + public static int getDefaultBgId(Context context) { + if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + NotesPreferenceActivity.PREFERENCE_SET_BG_COLOR_KEY, false)) { + return (int) (Math.random() * NoteBgResources.BG_EDIT_RESOURCES.length); + } else { + return BG_DEFAULT_COLOR; + } + }/*一个公共静态方法 getDefaultBgId(),它接受一个 Context 对象作为参数,并返回一个整型值。 + +在这个方法中,首先通过 PreferenceManager.getDefaultSharedPreferences(context) 获取了一个默认的 SharedPreferences 对象,并检查其中的 PREFERENCE_SET_BG_COLOR_KEY 是否为 true。如果是 true,则使用 Math.random() 随机生成一个范围在 NoteBgResources.BG_EDIT_RESOURCES 数组长度内的整数,并返回该整数作为默认背景颜色的 ID。如果 PREFERENCE_SET_BG_COLOR_KEY 是 false,则返回 BG_DEFAULT_COLOR,它是 ResourceParser 类中定义的默认背景颜色的静态常量。*/ + + public static class NoteItemBgResources { + private final static int [] BG_FIRST_RESOURCES = new int [] { + R.drawable.list_yellow_up, + R.drawable.list_blue_up, + R.drawable.list_white_up, + R.drawable.list_green_up, + R.drawable.list_red_up + };/*一个静态内部类 NoteItemBgResources,其中包含一个名为 BG_FIRST_RESOURCES 的私有静态常量数组。 + +该数组包含了 5 个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.list_yellow_up、R.drawable.list_blue_up、R.drawable.list_white_up、R.drawable.list_green_up、R.drawable.list_red_up。*/ + + private final static int [] BG_NORMAL_RESOURCES = new int [] { + R.drawable.list_yellow_middle, + R.drawable.list_blue_middle, + R.drawable.list_white_middle, + R.drawable.list_green_middle, + R.drawable.list_red_middle + };/*这是一个私有的静态常量数组 BG_NORMAL_RESOURCES,它包含了五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.list_yellow_middle、R.drawable.list_blue_middle、R.drawable.list_white_middle、R.drawable.list_green_middle、R.drawable.list_red_middle。 + +这个数组可能是用于为笔记列表项设置不同背景颜色的 drawable 资源数组,每个元素对应一种背景颜色。 +其中,R.drawable.list_yellow_middle、R.drawable.list_blue_middle、R.drawable.list_white_middle、R.drawable.list_green_middle、R.drawable.list_red_middle 对应的是列表项中间部分的背景颜色。*/ + private final static int [] BG_LAST_RESOURCES = new int [] { + R.drawable.list_yellow_down, + R.drawable.list_blue_down, + R.drawable.list_white_down, + R.drawable.list_green_down, + R.drawable.list_red_down, + };/*一个私有的静态常量数组 BG_LAST_RESOURCES,它包含了五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.list_yellow_down、R.drawable.list_blue_down、R.drawable.list_white_down、R.drawable.list_green_down、R.drawable.list_red_down。 + +这个数组可能是用于为笔记列表项设置不同背景颜色的 drawable 资源数组,每个元素对应一种背景颜色。其中,R.drawable.list_yellow_down、R.drawable.list_blue_down、R.drawable.list_white_down、R.drawable.list_green_down、R.drawable.list_red_down 对应的是列表项底部部分的背景颜色。*/ + + private final static int [] BG_SINGLE_RESOURCES = new int [] { + R.drawable.list_yellow_single, + R.drawable.list_blue_single, + R.drawable.list_white_single, + R.drawable.list_green_single, + R.drawable.list_red_single + };/*一个私有的静态常量数组 BG_SINGLE_RESOURCES,它包含了五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.list_yellow_single、R.drawable.list_blue_single、R.drawable.list_white_single、R.drawable.list_green_single、R.drawable.list_red_single。 + +这个数组可能是用于为笔记列表项设置不同背景颜色的 drawable 资源数组,每个元素对应一种背景颜色。其中,R.drawable.list_yellow_single、R.drawable.list_blue_single、R.drawable.list_white_single、R.drawable.list_green_single、R.drawable.list_red_single 对应的是只有一个列表项时的背景颜色。*/ + + public static int getNoteBgFirstRes(int id) { + return BG_FIRST_RESOURCES[id]; + }/*一个公共静态方法 getNoteBgFirstRes(),它接受一个整型参数 id,并返回一个整型值。*/ + + public static int getNoteBgLastRes(int id) { + return BG_LAST_RESOURCES[id]; + }/*一个公共静态方法 getNoteBgLastRes(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,静态常量数组 BG_LAST_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值*/ + + public static int getNoteBgSingleRes(int id) { + return BG_SINGLE_RESOURCES[id]; + }/*一个公共静态方法 getNoteBgSingleRes(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,静态常量数组 BG_SINGLE_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。*/ + + public static int getNoteBgNormalRes(int id) { + return BG_NORMAL_RESOURCES[id]; + }/*一个公共静态方法 getNoteBgNormalRes(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,静态常量数组 BG_NORMAL_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。*/ + + public static int getFolderBgRes() { + return R.drawable.list_folder; + } + }/*一个公共静态方法 getFolderBgRes(),它返回一个整型值。 + +在这个方法中,返回了一个名为 list_folder 的 drawable 资源的 ID,该资源可能是用于为文件夹列表项设置背景的。*/ + + public static class WidgetBgResources { + private final static int [] BG_2X_RESOURCES = new int [] { + R.drawable.widget_2x_yellow, + R.drawable.widget_2x_blue, + R.drawable.widget_2x_white, + R.drawable.widget_2x_green, + R.drawable.widget_2x_red, + };/*一个静态内部类 WidgetBgResources,它包含一个私有的静态常量数组 BG_2X_RESOURCES,该数组包含五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.widget_2x_yellow、R.drawable.widget_2x_blue、R.drawable.widget_2x_white、R.drawable.widget_2x_green、R.drawable.widget_2x_red。*/ + + public static int getWidget2xBgResource(int id) { + return BG_2X_RESOURCES[id]; + } + + private final static int [] BG_4X_RESOURCES = new int [] { + R.drawable.widget_4x_yellow, + R.drawable.widget_4x_blue, + R.drawable.widget_4x_white, + R.drawable.widget_4x_green, + R.drawable.widget_4x_red + };/*一个公共静态方法 getWidget2xBgResource(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,静态常量数组 BG_2X_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。 + +这个方法可能是用于获取小部件 2x2 大小的背景资源的方法,它接受一个背景颜色的 ID,返回对应的 drawable 资源。 + +这个静态内部类还包含了一个私有的静态常量数组 BG_4X_RESOURCES,该数组包含五个整型元素,这些整型元素对应着项目中的一些 drawable 资源 R.drawable.widget_4x_yellow、R.drawable.widget_4x_blue、R.drawable.widget_4x_white、R.drawable.widget_4x_green、R.drawable.widget_4x_red。*/ + + public static int getWidget4xBgResource(int id) { + return BG_4X_RESOURCES[id]; + } + }/*一个公共静态方法 getWidget4xBgResource(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,静态常量数组 BG_4X_RESOURCES 被索引到,以返回该数组中索引为 id 的元素的值。*/ + + public static class TextAppearanceResources { + private final static int [] TEXTAPPEARANCE_RESOURCES = new int [] { + R.style.TextAppearanceNormal, + R.style.TextAppearanceMedium, + R.style.TextAppearanceLarge, + R.style.TextAppearanceSuper + };/*一个静态内部类 TextAppearanceResources,它包含一个私有的静态常量数组 TEXTAPPEARANCE_RESOURCES,该数组包含四个整型元素,这些整型元素对应着项目中的一些样式资源 R.style.TextAppearanceNormal、R.style.TextAppearanceMedium、R.style.TextAppearanceLarge、R.style.TextAppearanceSuper。*/ + + public static int getTexAppearanceResource(int id) { + /** + * HACKME: Fix bug of store the resource id in shared preference. + * The id may larger than the length of resources, in this case, + * return the {@link ResourceParser#BG_DEFAULT_FONT_SIZE} + */ + if (id >= TEXTAPPEARANCE_RESOURCES.length) { + return BG_DEFAULT_FONT_SIZE; + } + return TEXTAPPEARANCE_RESOURCES[id]; + }/*一个公共静态方法 getTexAppearanceResource(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,首先判断传入的 id 是否大于等于 TEXTAPPEARANCE_RESOURCES 数组的长度,如果是,则返回默认的字体大小 BG_DEFAULT_FONT_SIZE。 + +如果 id 小于 TEXTAPPEARANCE_RESOURCES 数组的长度,则返回 TEXTAPPEARANCE_RESOURCES 数组中索引为 id 的元素的值。*/ + + public static int getResourcesSize() { + return TEXTAPPEARANCE_RESOURCES.length; + } + } +}/*一个公共静态方法 getTexAppearanceResource(),它接受一个整型参数 id,并返回一个整型值。 + +在这个方法中,首先判断传入的 id 是否大于等于 TEXTAPPEARANCE_RESOURCES 数组的长度,如果是,则返回默认的字体大小 BG_DEFAULT_FONT_SIZE。 + +如果 id 小于 TEXTAPPEARANCE_RESOURCES 数组的长度,则返回 TEXTAPPEARANCE_RESOURCES 数组中索引为 id 的元素的值。*/ diff --git a/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java b/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java new file mode 100644 index 0000000..735e9bd --- /dev/null +++ b/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java @@ -0,0 +1,410 @@ +/* + * 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.accounts.Account; +import android.accounts.AccountManager; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.text.TextUtils; +import android.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.gtask.remote.GTaskSyncService; + + +public class NotesPreferenceActivity extends PreferenceActivity {// 定义了一个常量字符串 PREFERENCE_NAME,表示 SharedPreferences 的名称 + public static final String PREFERENCE_NAME = "notes_preferences";// 定义了一个常量字符串 PREFERENCE_SYNC_ACCOUNT_NAME,表示同步账户名称的键名 + + public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";// 定义了一个常量字符串 PREFERENCE_LAST_SYNC_TIME,表示上一次同步时间的键名 + + public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";// 定义了一个常量字符串 PREFERENCE_SET_BG_COLOR_KEY,表示是否随机设置背景颜色的键名 + + public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";// 定义了一个常量字符串 PREFERENCE_SYNC_ACCOUNT_KEY,表示同步账户的键名 + + private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";// 定义了一个常量字符串 PREFERENCE_SYNC_ACCOUNT_KEY,表示同步账户的键名 + + private static final String AUTHORITIES_FILTER_KEY = "authorities";// 定义了一个常量字符串 AUTHORITIES_FILTER_KEY,表示过滤器的 authorities 键名 + + private PreferenceCategory mAccountCategory;// 声明了一个 PreferenceCategory 类型的成员变量 mAccountCategory,表示账户分类 + + + private GTaskReceiver mReceiver;// 声明了一个 GTaskReceiver 类型的成员变量 mReceiver,表示 GTask 接收器 + + + private Account[] mOriAccounts;// 声明了一个 Account 数组类型的成员变量 mOriAccounts,表示原始账户 + + private boolean mHasAddedAccount;// 声明了一个 boolean 类型的成员变量 mHasAddedAccount,表示是否已添加了账户 + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + /* using the app icon for navigation */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + addPreferencesFromResource(R.xml.preferences);// 加载 preferences.xml 文件中的 Preference + mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);// 从 preferences.xml 中找到同步账户分类,赋值给 mAccountCategory + + mReceiver = new GTaskReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME); + registerReceiver(mReceiver, filter);// 注册 GTaskReceiver 广播接收器,监听 Gtask 同步服务的消息 + + mOriAccounts = null; + View header = LayoutInflater.from(this).inflate(R.layout.settings_header, null); + getListView().addHeaderView(header, null, true);//将原始账户数组 mOriAccounts 设置为 null。 + } + + @Override + protected void onResume() { + super.onResume();// onStart 方法,表示 Activity 启动时调用的方 + + // need to set sync account automatically if user has added a new + // account + if (mHasAddedAccount) { + Account[] accounts = getGoogleAccounts(); + if (mOriAccounts != null && accounts.length > mOriAccounts.length) { + for (Account accountNew : accounts) { + boolean found = false; + for (Account accountOld : mOriAccounts) { + if (TextUtils.equals(accountOld.name, accountNew.name)) { + found = true; + break; + } + } + if (!found) { + setSyncAccount(accountNew.name); + break; + } + } + } + } + + refreshUI(); + }/*1. 调用父类的 onCreate() 方法,初始化 Activity。 +2. 设置应用图标作为导航按钮,以便用户可以返回上一个Activity或者返回应用的主界面。 +3. 加载 preferences.xml 文件中的 Preference。 +4. 从 preferences.xml 中找到同步账户分类,赋值给 mAccountCategory。 +5. 注册 GTaskReceiver 广播接收器,监听 GTask 同步服务的消息。 +6. 将原始账户数组 mOriAccounts 设置为 null。 +7. 通过 LayoutInflater 加载一个设置界面的头部布局 R.layout.settings_header。 +8. 将头部布局添加到 ListView 的头部。 +这段代码的作用是初始化设置界面,并设置导航按钮、注册广播接收器、加载头部布局。*/ + + @Override + protected void onDestroy() { + if (mReceiver != null) { + unregisterReceiver(mReceiver); + } + super.onDestroy(); + }/*这段代码是 Android 中的一个 Activity 的 onDestroy() 方法,主要进行了以下操作: +1. 判断 mReceiver 是否为 null,如果不为 null,则解除广播接收器的注册。 +2. 调用父类的 onDestroy() 方法,销毁 Activity。 +这段代码的目的是在 Activity 销毁时,解除之前注册的广播接收器,以避免出现内存泄漏的情况。在 Android 中,如果不及时解除广播接收器的注册,会导致广播接收器持有 Activity 的引用而无法被垃圾回收,从而导致内存泄漏。因此,在 Activity销毁前,需要手动解除广播接收器的注册,以确保程序的正常运行和内存的释放。*/ + + private void loadAccountPreference() { + mAccountCategory.removeAll();// 清空同步账户分类中的所有设置项 + + Preference accountPref = new Preference(this);// 创建一个新的 Preference 对象 accountPref + final String defaultAccount = getSyncAccountName(this);// 获取当前默认的同步账户名称 + accountPref.setTitle(getString(R.string.preferences_account_title));// 设置 accountPref 的标题和摘要 + accountPref.setSummary(getString(R.string.preferences_account_summary)); // 设置 accountPref 的点击事件监听器 + accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) {// 判断是否正在进行同步任务 + if (!GTaskSyncService.isSyncing()) { + if (TextUtils.isEmpty(defaultAccount)) { + // the first time to set account + showSelectAccountAlertDialog();//如果当前同步账户为空,则弹出选择账户对话框 + } else { + // if the account has already been set, we need to promp + // user about the risk + showChangeAccountConfirmAlertDialog(); + }// 如果已经设置了同步账户,则需要提示用户风险 + } else { + Toast.makeText(NotesPreferenceActivity.this, + R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT) + .show();// 如果正在进行同步任务,则提示用户无法更改账户 + } + return true;// 返回 true,表示点击事件已被处理 + } + }); + + mAccountCategory.addPreference(accountPref);// 将 accountPref 添加到同步账户分类中 + } + + private void loadSyncButton() { + Button syncButton = (Button) findViewById(R.id.preference_sync_button);// 获取同步按钮和上次同步时间的视图对 + TextView lastSyncTimeView = (TextView) findViewById(R.id.prefenerece_sync_status_textview);// 设置同步按钮的状态和点击事件监听器 + + // set button state + if (GTaskSyncService.isSyncing()) { + syncButton.setText(getString(R.string.preferences_button_sync_cancel)); // 如果正在同步任务中,则将按钮文本设置为“取消同步”,并添加取消同步的点击事件监听器 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.cancelSync(NotesPreferenceActivity.this); + } + }); + } else { + syncButton.setText(getString(R.string.preferences_button_sync_immediately));// 如果没有正在同步任务,则将按钮文本设置为“立即同步”,并添加开始同步的点击事件监听器 + syncButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + GTaskSyncService.startSync(NotesPreferenceActivity.this); + } + }); + }// 根据当前是否设置了同步账户来设置同步按钮的可用状态 + syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));// 设置上次同步时间的显示状态和文本内容 + + // set last sync time + if (GTaskSyncService.isSyncing()) { + lastSyncTimeView.setText(GTaskSyncService.getProgressString()); + lastSyncTimeView.setVisibility(View.VISIBLE); // 如果正在同步任务中,则显示当前同步进度,并将上次同步时间视图设置为可见状态 + } else { + long lastSyncTime = getLastSyncTime(this); + if (lastSyncTime != 0) { + lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time, + DateFormat.format(getString(R.string.preferences_last_sync_time_format), + lastSyncTime))); + lastSyncTimeView.setVisibility(View.VISIBLE); + } else { + lastSyncTimeView.setVisibility(View.GONE); + } + } + }// 如果没有正在同步任务,则获取上次同步时间,并根据上次同步时间是否为0来设置上次同步时间视图的可见状态和文本内容 + + private void refreshUI() { + loadAccountPreference(); + loadSyncButton(); + }/*loadAccountPreference() 方法用于加载和显示当前同步账户的信息,而 loadSyncButton() 方法则用于设置同步按钮的状态和点击事件监听器,并显示上次同步时间的信息。*/ + + private void showSelectAccountAlertDialog() { + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);// 创建 AlertDialog.Builder 对象 + + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);/// 创建自定义标题视图 + titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));// 设置标题文本 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));// 设置副标题文本 + + dialogBuilder.setCustomTitle(titleView);// 将自定义标题视图设置到对话框中 + dialogBuilder.setPositiveButton(null, null); // 不设置确定按钮和点击事件监听器 + + Account[] accounts = getGoogleAccounts(); + String defAccount = getSyncAccountName(this);// 获取所有谷歌账户的列表和当前同步账户的名称 + + mOriAccounts = accounts; + mHasAddedAccount = false;// 保存原始账户列表并将“已添加账户”标志设为 false + + if (accounts.length > 0) {// 如果找到了至少一个谷歌账户,则创建单选项列表 + CharSequence[] items = new CharSequence[accounts.length];// 创建单选项列表的选项文本数组 + final CharSequence[] itemMapping = items;// 创建选项文本数组的映射数组 + int checkedItem = -1; // 初始化默认选中项的索引为 -1 + int index = 0; + for (Account account : accounts) {// 遍历所有谷歌账户,为每个账户设置一个选项 + if (TextUtils.equals(account.name, defAccount)) { + checkedItem = index; + }// 如果该账户的名称与当前同步账户的名称相同,则将其作为默认选中项 + items[index++] = account.name;// 将该账户的名称添加到选项文本数组中 + } + dialogBuilder.setSingleChoiceItems(items, checkedItem, // 将单选项列表设置到对话框中,并为每个选项设置点击事件监听器 + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setSyncAccount(itemMapping[which].toString()); // 将用户选择的账户名称设置为同步账户 + dialog.dismiss();// 关闭对话框 + refreshUI();// 刷新 UI 界面上的账户和同步按钮状态 + } + }); + } + + View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);//从布局文件中加载一个视图对象并将其赋值给 addAccountView 变量 + dialogBuilder.setView(addAccountView);//将 addAccountView 设置为对话框的自定义视图 + + final AlertDialog dialog = dialogBuilder.show();//创建一个 AlertDialog 对象,并将其显示出来 + addAccountView.setOnClickListener(new View.OnClickListener() {//添加一个点击事件监听器 + public void onClick(View v) { + mHasAddedAccount = true;//用户是否已经添加了一个账户 + Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS"); + intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { + "gmail-ls" + });//创建了一个 Intent 对象,用于启动一个系统设置界面,以便让用户添加新的帐户 + startActivityForResult(intent, -1);//启动一个新的界面,等待用户添加新的账户 + dialog.dismiss();//关闭对话框 + } + }); + } + + private void showChangeAccountConfirmAlertDialog() { + // 创建一个 AlertDialog.Builder 对象 + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this); + + // 从布局文件 R.layout.account_dialog_title 中加载一个视图 titleView + View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null); + // 从 titleView 中找到一个 TextView 对象 titleTextView,并设置其文本 + TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title); + titleTextView.setText(getString(R.string.preferences_dialog_change_account_title, + getSyncAccountName(this))); + // 从 titleView 中找到一个 TextView 对象 subtitleTextView,并设置其文本 + TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle); + subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg)); + // 将 titleView 设置为对话框的自定义标题 + dialogBuilder.setCustomTitle(titleView); + + // 创建一个 CharSequence 类型的数组 menuItemArray,包含三个字符串,作为选项菜单的标签文字 + CharSequence[] menuItemArray = new CharSequence[] { + getString(R.string.preferences_menu_change_account), + getString(R.string.preferences_menu_remove_account), + getString(R.string.preferences_menu_cancel) + }; + dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() { // 创建一个 DialogInterface.OnClickListener 对象,用于处理选项菜单的点击事件 + public void onClick(DialogInterface dialog, int which) {// 判断点击了哪个菜单项 + if (which == 0) { + showSelectAccountAlertDialog();// 如果点击了第一个菜单项,调用 showSelectAccountAlertDialog() 方法显示“选择帐户”对话框 + } else if (which == 1) { + removeSyncAccount(); + refreshUI();// 如果点击了第二个菜单项,先调用 removeSyncAccount() 方法删除同步帐户,再调用 refreshUI() 方法刷新界面 + } + } + }); + dialogBuilder.show();// 显示对话框 + } + + private Account[] getGoogleAccounts() { + // 获取 AccountManager 对象 + AccountManager accountManager = AccountManager.get(this); + // 调用 getAccountsByType() 方法,传入参数 "com.google",以获取所有类型为 "com.google" 的帐户 + return accountManager.getAccountsByType("com.google"); + } + + private void setSyncAccount(String account) { + if (!getSyncAccountName(this).equals(account)) {// 判断传入的 account 是否与当前同步帐户相同 + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit();// 获取 SharedPreferences 对象 + if (account != null) { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account); // 如果传入的 account 不为 null,则将其存储到 SharedPreferences 中 + } else { + editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");// 如果传入的 account 为 null,则将空字符串存储到 SharedPreferences 中 + } + editor.commit();// 提交修改 + + // clean up last sync time + setLastSyncTime(this, 0);// 清除上一次同步的时间 + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues(); // 创建一个 ContentValues 对象,用于更新所有的 note + values.put(NoteColumns.GTASK_ID, "");// 清空 GTasks ID 和 sync ID + values.put(NoteColumns.SYNC_ID, 0);// 清空 GTasks ID 和 sync ID + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); // 更新所有的 note + } + }).start(); + + Toast.makeText(NotesPreferenceActivity.this, + getString(R.string.preferences_toast_success_set_accout, account), + Toast.LENGTH_SHORT).show();// 显示一个 Toast,提示同步帐户设置成功 + } + } + + private void removeSyncAccount() { + SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); // 获取 SharedPreferences 对象 + SharedPreferences.Editor editor = settings.edit();// 获取 SharedPreferences 对象 + if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) { + editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME); + }// 如果 SharedPreferences 包含同步帐户名称,则从 SharedPreferences 中删除该名称 + if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) { + editor.remove(PREFERENCE_LAST_SYNC_TIME); // 如果 SharedPreferences 包含上一次同步的时间,则从 SharedPreferences 中删除该时间 + } + editor.commit(); // 提交修改 + + // clean up local gtask related info + new Thread(new Runnable() { + public void run() { + ContentValues values = new ContentValues();// 创建一个 ContentValues 对象,用于更新所有的 note + values.put(NoteColumns.GTASK_ID, ""); + values.put(NoteColumns.SYNC_ID, 0); // 清空 GTasks ID 和 sync ID + getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null); + } + }).start();// 更新所有的 note + } + + public static String getSyncAccountName(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,// 获取 SharedPreferences 对象 + Context.MODE_PRIVATE); + return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");// 从 SharedPreferences 中获取同步帐户名称 + } + + public static void setLastSyncTime(Context context, long time) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE); + SharedPreferences.Editor editor = settings.edit(); // 获取 SharedPreferences 对象 + editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);// 将最后一次同步的时间保存到 SharedPreferences 中 + editor.commit();// 提交修改 + } + + public static long getLastSyncTime(Context context) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, + Context.MODE_PRIVATE);// 获取 SharedPreferences 对象 + return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);// 从 SharedPreferences 中获取最后一次同步的时间 + } + + private class GTaskReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + refreshUI();// 刷新用户界面 + if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) { + TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview); + syncStatus.setText(intent + .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));// 如果正在同步,则更新同步状态 + } + + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, NotesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; // 创建一个 Intent 对象,跳转到 NotesListActivity,并清除之前的所有 Activity + default: + return false;// 如果选择的菜单项不是返回主页,则返回 false + } + } +} diff --git a/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java new file mode 100644 index 0000000..5c7f03a --- /dev/null +++ b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java @@ -0,0 +1,153 @@ +/* + * 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.widget; +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import android.widget.RemoteViews; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import net.micode.notes.tool.ResourceParser; +import net.micode.notes.ui.NoteEditActivity; +import net.micode.notes.ui.NotesListActivity; + +public abstract class NoteWidgetProvider extends AppWidgetProvider { + public static final String [] PROJECTION = new String [] { + NoteColumns.ID, + NoteColumns.BG_COLOR_ID, + NoteColumns.SNIPPET + };/*一个抽象类 NoteWidgetProvider,它继承自 AppWidgetProvider 类。 + +在这个抽象类中定义了一个公共静态常量数组 PROJECTION,该数组包含三个字符串元素,这些字符串元素对应着笔记应用中的一些数据库列名,包括笔记的 ID、背景颜色 ID 和摘录内容。*/ + + public static final int COLUMN_ID = 0;// 笔记 ID 列在 PROJECTION 数组中的索引 + public static final int COLUMN_BG_COLOR_ID = 1;// 笔记背景颜色 ID 列在 PROJECTION 数组中的索引 + public static final int COLUMN_SNIPPET = 2;// 笔记摘录内容列在 PROJECTION 数组中的索引 + + private static final String TAG = "NoteWidgetProvider";// 日志输出标识符 + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); + for (int i = 0; i < appWidgetIds.length; i++) { + context.getContentResolver().update(Notes.CONTENT_NOTE_URI, + values, + NoteColumns.WIDGET_ID + "=?", + new String[] { String.valueOf(appWidgetIds[i])}); + } + }/*这是 NoteWidgetProvider 抽象类中的一个公共方法 onDeleted(),它重写了父类 AppWidgetProvider 中的方法,用于处理小部件被删除的事件。 + +在该方法中,首先创建了一个 ContentValues 对象 values,并将 NoteColumns.WIDGET_ID 的值设为 AppWidgetManager.INVALID_APPWIDGET_ID,表示小部件已被删除。 + +然后遍历传入的 appWidgetIds 数组,将每个小部件的 NoteColumns.WIDGET_ID 列更新为 AppWidgetManager.INVALID_APPWIDGET_ID,以便将小部件与笔记数据库中的记录解除绑定。 + +具体地,使用 getContentResolver() 获取一个 ContentResolver 对象,调用 update() 方法对笔记数据库中的记录进行更新。更新的条件为 NoteColumns.WIDGET_ID + "=?",即 NoteColumns.WIDGET_ID 等于当前小部件 ID,更新的数据为 values 对象,其中 NoteColumns.WIDGET_ID 的值已经被设置为 AppWidgetManager.INVALID_APPWIDGET_ID。*/ + + private Cursor getNoteWidgetInfo(Context context, int widgetId) {// 使用 getContentResolver() 方法获取 ContentResolver 对象,通过该对象进行对笔记数据库的查询操作 + return context.getContentResolver().query(Notes.CONTENT_NOTE_URI, // 使用 query() 方法查询笔记数据库,返回一个 Cursor 对象 + return context.getContentResolver().query(// 查询的 URI,笔记数据库中笔记的内容保存在该 URI 下 + PROJECTION, + Notes.CONTENT_NOTE_URI, + PROJECTION, + NoteColumns.WIDGET_ID + "=? AND " + NoteColumns.PARENT_ID + "<>?",// 查询的列,即笔记 ID、笔记背景颜色 ID 和笔记摘录内容 + new String[] { String.valueOf(widgetId), String.valueOf(Notes.ID_TRASH_FOLER) },//查询的条件,即笔记关联的小部件 ID 以及笔记的父 ID 不为回收站的笔记 + null);// 排序方式,这里为 null 表示不排序 + } + + protected void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(context, appWidgetManager, appWidgetIds, false); + }/*这是 NoteWidgetProvider 抽象类中的一个受保护的方法 update(),它接受一个 Context 对象、一个 AppWidgetManager 对象和一个整型数组 appWidgetIds 作为参数,并在方法中调用了另一个同名方法。*/ + + private void update(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds, + boolean privacyMode) { + for (int i = 0; i < appWidgetIds.length; i++) { + if (appWidgetIds[i] != AppWidgetManager.INVALID_APPWIDGET_ID) { + int bgId = ResourceParser.getDefaultBgId(context); + String snippet = ""; + Intent intent = new Intent(context, NoteEditActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_ID, appWidgetIds[i]); + intent.putExtra(Notes.INTENT_EXTRA_WIDGET_TYPE, getWidgetType()); + + Cursor c = getNoteWidgetInfo(context, appWidgetIds[i]); + if (c != null && c.moveToFirst()) { + if (c.getCount() > 1) { + Log.e(TAG, "Multiple message with same widget id:" + appWidgetIds[i]); + c.close(); + return; + } + snippet = c.getString(COLUMN_SNIPPET); + bgId = c.getInt(COLUMN_BG_COLOR_ID); + intent.putExtra(Intent.EXTRA_UID, c.getLong(COLUMN_ID)); + intent.setAction(Intent.ACTION_VIEW); + } else { + snippet = context.getResources().getString(R.string.widget_havenot_content); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + } + + if (c != null) { + c.close(); + } + + RemoteViews rv = new RemoteViews(context.getPackageName(), getLayoutId()); + rv.setImageViewResource(R.id.widget_bg_image, getBgResourceId(bgId)); + intent.putExtra(Notes.INTENT_EXTRA_BACKGROUND_ID, bgId); + /** + * Generate the pending intent to start host for the widget + */ + PendingIntent pendingIntent = null; + if (privacyMode) { + rv.setTextViewText(R.id.widget_text, + context.getString(R.string.widget_under_visit_mode)); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], new Intent( + context, NotesListActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + } else { + rv.setTextViewText(R.id.widget_text, snippet); + pendingIntent = PendingIntent.getActivity(context, appWidgetIds[i], intent, + PendingIntent.FLAG_UPDATE_CURRENT); + } + + rv.setOnClickPendingIntent(R.id.widget_text, pendingIntent); + appWidgetManager.updateAppWidget(appWidgetIds[i], rv); + } + } + }/*这是 `NoteWidgetProvider` 抽象类中的一个私有方法 `update()`,它接受一个 `Context` 对象、一个 `AppWidgetManager` 对象、一个整型数组 `appWidgetIds` 和一个布尔型参数 `privacyMode` 作为参数。该方法用于更新小部件的显示内容和点击事件。 + +方法中首先对 `appWidgetIds` 数组进行遍历,对于每个小部件 ID,如果它不等于 `AppWidgetManager.INVALID_APPWIDGET_ID`,则执行以下操作: + +- 获取小部件的默认背景 ID,以及一个空的摘录字符串。 +- 创建一个 `NoteEditActivity` 的意图,并将小部件 ID、小部件类型等信息作为附加数据放入其中。 +- 调用 `getNoteWidgetInfo()` 方法获取与当前小部件关联的笔记信息,并根据结果设置摘录字符串、背景 ID 和意图的操作类型。 +- 根据背景 ID 创建一个 `RemoteViews` 对象,并将背景图片、背景 ID 和点击事件所需的 `PendingIntent` 添加到其中。 +- 调用 `AppWidgetManager` 的 `updateAppWidget()` 方法更新小部件的显示内容。 + +如果 `privacyMode` 为 `true`,则小部件将显示 "隐私模式",并且点击小部件将启动 `NotesListActivity`;否则,小部件将显示与笔记关联的摘录字符串,点击小部件将启动 `NoteEditActivity`。*/ + + protected abstract int getBgResourceId(int bgId);//用于获取小部件的背景资源 ID。 + + protected abstract int getLayoutId();//用于获取小部件的布局资源 ID。 + + protected abstract int getWidgetType();//用于获取小部件的类型。 +} diff --git a/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java new file mode 100644 index 0000000..43921a1 --- /dev/null +++ b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java @@ -0,0 +1,47 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + + +public class NoteWidgetProvider_2x extends NoteWidgetProvider {//创建一个继承自 NoteWidgetProvider 的子类 NoteWidgetProvider_2x + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + }/*重写父类的 onUpdate() 方法,并在其中调用父类的 update() 方法,以更新小部件的显示内容和点击事件。*/ + + @Override + protected int getLayoutId() { + return R.layout.widget_2x; + }/*重写父类的 getLayoutId() 方法,返回用于小部件的布局资源 ID。这里返回 R.layout.widget_2x,表示使用 widget_2x.xml 文件作为布局资源。*/ + + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId); + }/*重写父类的 getBgResourceId() 方法,返回用于小部件的背景资源 ID。这里调用了 ResourceParser.WidgetBgResources.getWidget2xBgResource(bgId) 方法,该方法根据传入的背景 ID 返回相应的背景资源 ID。*/ + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_2X; + }/*重写父类的 getWidgetType() 方法,返回小部件的类型。这里返回 Notes.TYPE_WIDGET_2X,表示这是一个 2x 大小的小部件。*/ +} diff --git a/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java new file mode 100644 index 0000000..7c6c512 --- /dev/null +++ b/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java @@ -0,0 +1,45 @@ +/* + * 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.widget; + +import android.appwidget.AppWidgetManager; +import android.content.Context; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + + +public class NoteWidgetProvider_4x extends NoteWidgetProvider {//创建一个继承自 NoteWidgetProvider 的子类 NoteWidgetProvider_4x。 + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.update(context, appWidgetManager, appWidgetIds); + }/*重写父类的 onUpdate() 方法,并在其中调用父类的 update() 方法,以更新小部件的显示内容和点击事件。*/ + + protected int getLayoutId() { + return R.layout.widget_4x; + }/*重写父类的 getLayoutId() 方法,返回用于小部件的布局资源 ID。这里返回 R.layout.widget_4x,表示使用 widget_4x.xml 文件作为布局资源。*/ + @Override + protected int getBgResourceId(int bgId) { + return ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId); + }/*重写父类的 getBgResourceId() 方法,返回用于小部件的背景资源 ID。这里调用了 ResourceParser.WidgetBgResources.getWidget4xBgResource(bgId) 方法,该方法根据传入的背景 ID 返回相应的背景资源 ID。*/ + + @Override + protected int getWidgetType() { + return Notes.TYPE_WIDGET_4X; + } +}/*重写父类的 getWidgetType() 方法,返回小部件的类型。这里返回 Notes.TYPE_WIDGET_4X,表示这是一个 4x 大小的小部件。*/