You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							413 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							413 lines
						
					
					
						
							15 KiB
						
					
					
				| /*
 | |
|  * 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.Build;
 | |
| 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.BufferedWriter;
 | |
| import java.io.File;
 | |
| import java.io.FileOutputStream;
 | |
| import java.io.IOException;
 | |
| import java.io.OutputStreamWriter;
 | |
| import java.nio.charset.StandardCharsets;
 | |
| import java.util.ArrayList;
 | |
| import java.util.List;
 | |
| 
 | |
| 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;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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;
 | |
|     // Permission denied
 | |
|     public static final int STATE_PERMISSION_DENIED            = 5;
 | |
| 
 | |
|     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 String getExportedTextFileName() {
 | |
|         return mTextExport.mFileName;
 | |
|     }
 | |
| 
 | |
|     public String getExportedTextFileDir() {
 | |
|         return mTextExport.mFileDirectory;
 | |
|     }
 | |
| 
 | |
|     private static class TextExport {
 | |
|         private static final String[] NOTE_PROJECTION = {
 | |
|                 NoteColumns.ID,
 | |
|                 NoteColumns.MODIFIED_DATE,
 | |
|                 NoteColumns.SNIPPET,
 | |
|                 NoteColumns.TYPE,
 | |
|                 NoteColumns.PARENT_ID
 | |
|         };
 | |
| 
 | |
|         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 int NOTE_COLUMN_TYPE = 3;
 | |
|         private static final int NOTE_COLUMN_PARENT_ID = 4;
 | |
| 
 | |
|         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;
 | |
| 
 | |
|         private Context mContext;
 | |
|         private String mFileName;
 | |
|         private String mFileDirectory;
 | |
|         private BufferedWriter mWriter;
 | |
|         private int mNotesCount;
 | |
|         private int mFoldersCount;
 | |
| 
 | |
|         public TextExport(Context context) {
 | |
|             TEXT_FORMAT = context.getResources().getStringArray(R.array.format_for_exported_note);
 | |
|             mContext = context;
 | |
|             mFileName = "";
 | |
|             mFileDirectory = "";
 | |
|             mNotesCount = 0;
 | |
|             mFoldersCount = 0;
 | |
|         }
 | |
| 
 | |
|         private String getFormat(int id) {
 | |
|             return TEXT_FORMAT[id];
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * Export notes to text file
 | |
|          */
 | |
|         public int exportToText() {
 | |
|             if (!externalStorageAvailable()) {
 | |
|                 Log.d(TAG, "Media was not mounted");
 | |
|                 return STATE_SD_CARD_UNMOUONTED;
 | |
|             }
 | |
| 
 | |
|             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 STATE_SYSTEM_ERROR;
 | |
|             }
 | |
|             
 | |
|             mFileName = file.getName();
 | |
|             mFileDirectory = mContext.getString(R.string.file_path);
 | |
|             
 | |
|             // 使用try-with-resources确保资源释放
 | |
|             try (BufferedWriter writer = new BufferedWriter(
 | |
|                     new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
 | |
|                 mWriter = writer;
 | |
|                 
 | |
|                 // 导出文件夹和笔记
 | |
|                 exportFoldersAndNotes();
 | |
|                 
 | |
|                 // 写入导出统计信息
 | |
|                 writeExportSummary();
 | |
|                 
 | |
|                 Log.d(TAG, "Export successful. Exported " + mFoldersCount + " folders and " + mNotesCount + " notes.");
 | |
|                 return STATE_SUCCESS;
 | |
|             } catch (IOException e) {
 | |
|                 Log.e(TAG, "Error during export: " + e.getMessage(), e);
 | |
|                 return STATE_SYSTEM_ERROR;
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 写入导出摘要信息
 | |
|          */
 | |
|         private void writeExportSummary() throws IOException {
 | |
|             mWriter.write("\n");
 | |
|             mWriter.write("==============================\n");
 | |
|             mWriter.write(mContext.getString(R.string.export_summary) + "\n");
 | |
|             mWriter.write(mContext.getString(R.string.export_date) + ": " 
 | |
|                     + DateFormat.format(mContext.getString(R.string.format_datetime_ymdhm), System.currentTimeMillis()) + "\n");
 | |
|             mWriter.write(mContext.getString(R.string.folder_count) + ": " + mFoldersCount + "\n");
 | |
|             mWriter.write(mContext.getString(R.string.note_count) + ": " + mNotesCount + "\n");
 | |
|             mWriter.write("==============================\n");
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出所有文件夹和笔记
 | |
|          */
 | |
|         private void exportFoldersAndNotes() throws IOException {
 | |
|             // 首先导出文件夹和其中的笔记
 | |
|             exportFolders();
 | |
|             
 | |
|             // 导出根文件夹中的笔记
 | |
|             exportRootNotes();
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出所有文件夹
 | |
|          */
 | |
|         private void exportFolders() throws IOException {
 | |
|             // 查询所有文件夹
 | |
|             String selection = "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND "
 | |
|                     + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER + ") OR "
 | |
|                     + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER;
 | |
|             
 | |
|             try (Cursor folderCursor = mContext.getContentResolver().query(
 | |
|                     Notes.CONTENT_NOTE_URI,
 | |
|                     NOTE_PROJECTION,
 | |
|                     selection, null, null)) {
 | |
|                 
 | |
|                 if (folderCursor != null && folderCursor.moveToFirst()) {
 | |
|                     do {
 | |
|                         mFoldersCount++;
 | |
|                         exportFolder(folderCursor);
 | |
|                     } while (folderCursor.moveToNext());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出单个文件夹及其包含的笔记
 | |
|          */
 | |
|         private void exportFolder(Cursor folderCursor) throws IOException {
 | |
|             String folderId = folderCursor.getString(NOTE_COLUMN_ID);
 | |
|             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)) {
 | |
|                 mWriter.write(String.format(getFormat(FORMAT_FOLDER_NAME), folderName));
 | |
|                 mWriter.newLine();
 | |
|             }
 | |
|             
 | |
|             // 导出文件夹中的笔记
 | |
|             exportNotesInFolder(folderId);
 | |
|             
 | |
|             // 添加文件夹分隔线
 | |
|             mWriter.write("\n");
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出指定文件夹中的所有笔记
 | |
|          */
 | |
|         private void exportNotesInFolder(String folderId) throws IOException {
 | |
|             try (Cursor notesCursor = mContext.getContentResolver().query(Notes.CONTENT_NOTE_URI,
 | |
|                     NOTE_PROJECTION, NoteColumns.PARENT_ID + "=?", new String[] { folderId }, null)) {
 | |
|                 
 | |
|                 if (notesCursor != null && notesCursor.moveToFirst()) {
 | |
|                     do {
 | |
|                         mNotesCount++;
 | |
|                         exportNote(notesCursor);
 | |
|                     } while (notesCursor.moveToNext());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出根文件夹中的笔记
 | |
|          */
 | |
|         private void exportRootNotes() throws IOException {
 | |
|             try (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 && noteCursor.moveToFirst()) {
 | |
|                     do {
 | |
|                         mNotesCount++;
 | |
|                         exportNote(noteCursor);
 | |
|                     } while (noteCursor.moveToNext());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出单个笔记
 | |
|          */
 | |
|         private void exportNote(Cursor noteCursor) throws IOException {
 | |
|             // 打印笔记的最后修改日期
 | |
|             mWriter.write(String.format(getFormat(FORMAT_NOTE_DATE), 
 | |
|                     DateFormat.format(mContext.getString(R.string.format_datetime_mdhm),
 | |
|                             noteCursor.getLong(NOTE_COLUMN_MODIFIED_DATE))));
 | |
|             mWriter.newLine();
 | |
|             
 | |
|             // 查询并导出笔记的数据
 | |
|             String noteId = noteCursor.getString(NOTE_COLUMN_ID);
 | |
|             exportNoteData(noteId);
 | |
|             
 | |
|             // 添加笔记分隔线
 | |
|             mWriter.write("\n");
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出笔记的所有数据
 | |
|          */
 | |
|         private void exportNoteData(String noteId) throws IOException {
 | |
|             try (Cursor dataCursor = mContext.getContentResolver().query(Notes.CONTENT_DATA_URI,
 | |
|                     DATA_PROJECTION, DataColumns.NOTE_ID + "=?", new String[] { noteId }, null)) {
 | |
|                 
 | |
|                 if (dataCursor != null && dataCursor.moveToFirst()) {
 | |
|                     do {
 | |
|                         String mimeType = dataCursor.getString(DATA_COLUMN_MIME_TYPE);
 | |
|                         if (DataConstants.CALL_NOTE.equals(mimeType)) {
 | |
|                             exportCallNote(dataCursor);
 | |
|                         } else if (DataConstants.NOTE.equals(mimeType)) {
 | |
|                             exportTextNote(dataCursor);
 | |
|                         }
 | |
|                     } while (dataCursor.moveToNext());
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出通话记录笔记
 | |
|          */
 | |
|         private void exportCallNote(Cursor dataCursor) throws IOException {
 | |
|             // 打印电话号码
 | |
|             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)) {
 | |
|                 mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), phoneNumber));
 | |
|                 mWriter.newLine();
 | |
|             }
 | |
|             
 | |
|             // 打印通话日期
 | |
|             mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), 
 | |
|                     DateFormat.format(mContext.getString(R.string.format_datetime_mdhm), callDate)));
 | |
|             mWriter.newLine();
 | |
|             
 | |
|             // 打印通话附件位置
 | |
|             if (!TextUtils.isEmpty(location)) {
 | |
|                 mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), location));
 | |
|                 mWriter.newLine();
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         /**
 | |
|          * 导出文本笔记
 | |
|          */
 | |
|         private void exportTextNote(Cursor dataCursor) throws IOException {
 | |
|             String content = dataCursor.getString(DATA_COLUMN_CONTENT);
 | |
|             if (!TextUtils.isEmpty(content)) {
 | |
|                 mWriter.write(String.format(getFormat(FORMAT_NOTE_CONTENT), content));
 | |
|                 mWriter.newLine();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * Generate the text file to store imported data
 | |
|          */
 | |
|         private static File generateFileMountedOnSDcard(Context context, int filePathResId, int fileNameFormatResId) {
 | |
|             StringBuilder sb = new StringBuilder();
 | |
|             
 | |
|             // 适配Android 10+的存储访问
 | |
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
 | |
|                 sb.append(context.getExternalFilesDir(null));
 | |
|             } else {
 | |
|                 sb.append(Environment.getExternalStorageDirectory());
 | |
|             }
 | |
|             
 | |
|             sb.append(context.getString(filePathResId));
 | |
|             File filedir = new File(sb.toString());
 | |
|             
 | |
|             // 确保目录存在
 | |
|             if (!filedir.exists() && !filedir.mkdirs()) {
 | |
|                 Log.e(TAG, "Failed to create directory: " + filedir.getAbsolutePath());
 | |
|                 return null;
 | |
|             }
 | |
|             
 | |
|             // 构建文件名
 | |
|             sb.append(context.getString(
 | |
|                     fileNameFormatResId,
 | |
|                     DateFormat.format(context.getString(R.string.format_date_ymd),
 | |
|                             System.currentTimeMillis())));
 | |
|             File file = new File(sb.toString());
 | |
| 
 | |
|             try {
 | |
|                 if (!file.exists() && !file.createNewFile()) {
 | |
|                     Log.e(TAG, "Failed to create file: " + file.getAbsolutePath());
 | |
|                     return null;
 | |
|                 }
 | |
|                 return file;
 | |
|             } catch (SecurityException | IOException e) {
 | |
|                 Log.e(TAG, "Error creating file: " + e.getMessage(), e);
 | |
|                 return null;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 |