From b67dc5c1293cd94b439edc8479690d08d3034d16 Mon Sep 17 00:00:00 2001 From: zk <2930705585@qq.com> Date: Sun, 25 Jan 2026 08:06:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=A0=E5=AF=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/Notes.java | 35 +---- src/tool/DataUtils.java | 143 +++++++++++++------ src/ui/EncryptedFolderManager.java | 218 +++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 src/ui/EncryptedFolderManager.java diff --git a/src/data/Notes.java b/src/data/Notes.java index 6273380..8142bef 100644 --- a/src/data/Notes.java +++ b/src/data/Notes.java @@ -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 - * 如果是文件夹,存储文件夹名字 - * 如果是便签,存储便签摘要 *

Type: TEXT

*/ 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 *

Type: Integer 1:check list mode 0: normal mode

- * 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 *

Type: INTEGER (long)

- * data1表示通话时间戳 */ public static final String CALL_DATE = DATA1; /** * Phone number for this record *

Type: TEXT

- * data3表示电话号码 */ public static final String PHONE_NUMBER = DATA3; diff --git a/src/tool/DataUtils.java b/src/tool/DataUtils.java index 3c3e580..93157d8 100644 --- a/src/tool/DataUtils.java +++ b/src/tool/DataUtils.java @@ -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 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 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)); - //创建一个 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 ids, long folderId) { if (ids == null) { @@ -93,9 +91,10 @@ public class DataUtils { ArrayList operationList = new ArrayList(); 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 不等于回收站 ID(ID_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 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 便签ID的HashSet + */ + public static HashSet 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 ids = new HashSet(); + 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 ids, + long originFolderId) { + if (ids == null) { + Log.d(TAG, "the ids is null"); + return true; + } + ArrayList operationList = new ArrayList(); + 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) }); + } + } + + diff --git a/src/ui/EncryptedFolderManager.java b/src/ui/EncryptedFolderManager.java new file mode 100644 index 0000000..326177c --- /dev/null +++ b/src/ui/EncryptedFolderManager.java @@ -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; + } + } +} \ No newline at end of file