Database trong Android – Backup and Import
Bài đăng này đã không được cập nhật trong 8 năm
Trong bài viết này mình sẽ hướng dẫn các bạn cách Backup and Import database trong android.
Bước 1: Cho phép đọc ghi dữ liệu xuống thẻ nhớ trong AndroidManifest.xml
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.techdb.demodatabase" >
<!-- set can read and write on sd card-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".NoteActivity"
android:label="@string/title_activity_note" >
</activity>
</application>
</manifest>
Bước 2: Tạo menu ở MainActivity cho phép Backup và Import
Cập nhật file menu_main.xml – file menu của MainActivity res/menu/menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/menu_more"
android:icon="@drawable/ic_more"
android:title="Menu"
app:showAsAction="always">
<menu>
<item
android:id="@+id/menu_backup"
android:orderInCategory="100"
android:title="@string/backup_data"
app:showAsAction="never" />
<item
android:id="@+id/menu_import"
android:orderInCategory="100"
android:title="@string/import_data"
app:showAsAction="never" />
</menu>
</item>
</menu>
Các bạn chú ý ở trên mình có 1 item – item này chính là nút 3 chấm (overflow). Khi click vào nút 3 chấm sẽ hiện 2 menu Backup và Import (xem video) chính là 2 item bên trong nó.
Cập nhật file MainActivity.java để bắt sự kiện menu: MainActivity.java
package com.techdb.demodatabase;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements View.OnClickListener, BackupData.OnBackupListener {
private ItemNoteAdapter adapter;
private List<Note> listNote = new ArrayList<>();
private Context context;
private DatabaseHelper db;
private BackupData backupData;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = this;
db = new DatabaseHelper(context);
backupData = new BackupData(context);
backupData.setOnBackupListener(this);
connectView();
}
/**
* connect java with xml view
*/
private void connectView() {
// find Float Action Button
findViewById(R.id.fab).setOnClickListener(this);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_note);
// If the size of views will not change as the data changes.
recyclerView.setHasFixedSize(true);
// Setting the LayoutManager.
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
// Setting the adapter.
adapter = new ItemNoteAdapter(context, listNote);
recyclerView.setAdapter(adapter);
}
/**
* update list note when resume (open app or finish NoteActivity)
*/
public void onResume() {
super.onResume();
updateListNote();
}
/**
* select all note from database and set to ls
* use for loop to add into listNote.
* We must add all item in ls into listNote then adapter can update
* we add reverse ls to show new note at top of list
*/
private void updateListNote() {
// clear old list
listNote.clear();
// add all notes from database, reverse list
ArrayList<Note> ls = db.getListNote("SELECT * FROM " + DatabaseHelper.TABLE_NOTE);
// reverse list
for (int i = ls.size() - 1; i >= 0; i--) {
listNote.add(ls.get(i));
}
adapter.notifyDataSetChanged();
}
/**
* display note have id
*/
public static void showNote(Context context, long id) {
Intent intent = new Intent(context, NoteActivity.class);
// send id to NoteActivity
intent.putExtra(NoteActivity.ID, id);
context.startActivity(intent);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.fab:
showNote(context, NoteActivity.NEW_NOTE);
break;
default:
break;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_backup:
backupData.exportToSD();
break;
case R.id.menu_import:
backupData.importFromSD();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onFinishExport(String error) {
String notify = error;
if (error == null) {
notify = "Export success";
}
Toast.makeText(context, notify, Toast.LENGTH_SHORT).show();
}
@Override
public void onFinishImport(String error) {
String notify = error;
if (error == null) {
notify = "Import success";
updateListNote();
}
Toast.makeText(context, notify, Toast.LENGTH_SHORT).show();
}
}
Các bạn lưu ý mình có implement interface BackupData.OnBackupListener trong class BackupData (lát nữa sẽ viết) và viết đè 2 phương thức onFinishExport và onFinishImport của nó, 2 phương thức này cho phép chúng ta thông báo hoặc xử lý sau khi thực hiện backup và import xong.
Khi bắt sự kiện menu mình gọi 2 phương thức backupData.exportToSD và backupData.importFromSD để thực hiện.
Bước 3: Thực hiện viết Class BackupData
Trong bước này mình mô tả trước. Khi người dùng chọn backupdata thì chúng ta sẽ sao lưu data sang thẻ nhớ nằm ở thư mục nào đó, trong này mình cho nó lưu ở thư mục MyNote. Chương trình sẽ tự động tạo thư mục này nếu nó chưa tồn tại. Tên của file được backup sẽ là tên database kèm theo thời gian ngày tháng năm, giờ phút giây như trong video đầu bài.
Khi người dùng chọn Import data, chúng ta sẽ hỏi có muốn backup dữ liệu hiện tại trước khi import. Nếu có thì sao lưu rồi liệt kê danh sách file đã backup trước đó để import. Nếu không thì chỉ liệt kê file để import thôi.
Để tránh mât mát dữ liệu và tránh lỗi trong trường hợp data hiện tại có cấu trúc mới hơi data cũ, Chúng ta xóa toàn bộ bản ghi của data hiện tại rồi sao chép file backup vào một database tạm (temp database) và sau đó mới copy toàn bộ bảng và bản ghi ở databse tạm vào data hiện tại. Ví dụ data hiện tại của bạn có bảng note, bảng này có cột last_modified mà trong databse cũ không có, khi đó nếu sao chép trực tiếp sẽ dẫn đến database không có cột last_modified và làm ứng dụng bị lỗi.
Dưới đây là code đã giải thích tương đối rõ ràng. Các bạn có thể đọc hiểu. BackupData.java
package com.techdb.demodatabase;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Environment;
import android.support.v4.app.FragmentActivity;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
public class BackupData {
// url for database
private final String dataPath = "//data//com.techab.demodatabase//databases//";
// name of main data
private final String dataName = DatabaseHelper.DATABASE_NAME;
// data main
private final String data = dataPath + dataName;
// name of temp data
private final String dataTempName = DatabaseHelper.DATABASE_NAME + "_temp";
// temp data for copy data from sd then copy data temp into main data
private final String dataTemp = dataPath + dataTempName;
// folder on sd to backup data
private final String folderSD = Environment.getExternalStorageDirectory() + "/MyNote";
private Context context;
public BackupData(Context context) {
this.context = context;
}
// create folder if it not exist
private void createFolder() {
File sd = new File(folderSD);
if (!sd.exists()) {
sd.mkdir();
System.out.println("create folder");
} else {
System.out.println("exits");
}
}
/**
* Copy database to sd card
* name of file = database name + time when copy
* When finish, we call onFinishExport method to send notify for activity
*/
public void exportToSD() {
String error = null;
try {
createFolder();
File sd = new File(folderSD);
if (sd.canWrite()) {
SimpleDateFormat formatTime = new SimpleDateFormat("yyyy_MM_dd__HH_mm_ss");
String backupDBPath = dataName + "_" + formatTime.format(new Date());
File currentDB = new File(Environment.getDataDirectory(), data);
File backupDB = new File(sd, backupDBPath);
if (currentDB.exists()) {
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(backupDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} else {
System.out.println("db not exist");
}
}
} catch (Exception e) {
e.printStackTrace();
error = "Error backup";
}
onBackupListener.onFinishExport(error);
}
/**
* import data from file backup on sd card
* we must create a temp database for copy file on sd card to it.
* Then we copy all row of temp database into main database.
* It will keep struct of curren database not change when struct backup database is old
*
* @param fileNameOnSD name of file database backup on sd card
*/
public void importData(String fileNameOnSD) {
File sd = new File(folderSD);
// create temp database
SQLiteDatabase dbBackup = context.openOrCreateDatabase(dataTempName,
SQLiteDatabase.CREATE_IF_NECESSARY, null);
String error = null;
if (sd.canWrite()) {
File currentDB = new File(Environment.getDataDirectory(), dataTemp);
File backupDB = new File(sd, fileNameOnSD);
if (currentDB.exists()) {
FileChannel src;
try {
src = new FileInputStream(backupDB).getChannel();
FileChannel dst = new FileOutputStream(currentDB)
.getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
error = "Error load file";
} catch (IOException e) {
error = "Error import";
}
}
}
/**
*when copy old database into temp database success
* we copy all row of table into main database
*/
if (error == null) {
new CopyDataAsyncTask(dbBackup).execute();
} else {
onBackupListener.onFinishImport(error);
}
}
/**
* show dialog for select backup database before import database
* if user select yes, we will export curren database
* then show dialog to select old database to import
* else we onoly show dialog to select old database to import
*/
public void importFromSD() {
final AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppCompatAlertDialogStyle);
builder.setTitle(R.string.backup_data).setIcon(R.mipmap.ic_launcher)
.setMessage(R.string.backup_before_import);
builder.setPositiveButton(R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
showDialogListFile(folderSD);
}
});
builder.setNegativeButton(R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
showDialogListFile(folderSD);
exportToSD();
}
});
builder.show();
}
/**
* show dialog list all backup file on sd card
* @param forderPath folder conatain backup file
*/
private void showDialogListFile(String forderPath) {
createFolder();
File forder = new File(forderPath);
File[] listFile = forder.listFiles();
final String[] listFileName = new String[listFile.length];
for (int i = 0, j = listFile.length - 1; i < listFile.length; i++, j--) {
listFileName[j] = listFile[i].getName();
}
if (listFileName.length > 0) {
// get layout for list
LayoutInflater inflater = ((FragmentActivity) context).getLayoutInflater();
View convertView = (View) inflater.inflate(R.layout.list_backup_file, null);
final AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppCompatAlertDialogStyle);
// set view for dialog
builder.setView(convertView);
builder.setTitle(R.string.select_file).setIcon(R.mipmap.ic_launcher);
final AlertDialog alert = builder.create();
ListView lv = (ListView) convertView.findViewById(R.id.lv_backup);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(context,
android.R.layout.simple_list_item_1, listFileName);
lv.setAdapter(adapter);
lv.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
alert.dismiss();
importData(listFileName[position]);
}
});
alert.show();
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppCompatAlertDialogStyle);
builder.setTitle(R.string.delete).setIcon(R.mipmap.ic_launcher)
.setMessage(R.string.backup_empty);
builder.show();
}
}
/**
* AsyncTask for copy data
*/
class CopyDataAsyncTask extends AsyncTask<Void, Void, Void> {
ProgressDialog progress = new ProgressDialog(context);
SQLiteDatabase db;
public CopyDataAsyncTask(SQLiteDatabase dbBackup) {
this.db = dbBackup;
}
/**
* will call first
*/
@Override
protected void onPreExecute() {
// TODO Auto-generated method stub
super.onPreExecute();
progress.setMessage("Importing...");
progress.show();
}
@Override
protected Void doInBackground(Void... params) {
copyData(db);
return null;
}
/**
* end process
*/
@Override
protected void onPostExecute(Void error) {
// TODO Auto-generated method stub
super.onPostExecute(error);
if (progress.isShowing()) {
progress.dismiss();
}
onBackupListener.onFinishImport(null);
}
}
/**
* copy all row of temp database into main database
* @param dbBackup
*/
private void copyData(SQLiteDatabase dbBackup) {
DatabaseHelper db = new DatabaseHelper(context);
db.deleteNote(null);
/** copy all row of subject table */
Cursor cursor = dbBackup.query(true, DatabaseHelper.TABLE_NOTE, null, null, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
Note note = db.cursorToNote(cursor);
db.insertNote(note);
cursor.moveToNext();
}
cursor.close();
context.deleteDatabase(dataTempName);
}
private OnBackupListener onBackupListener;
public void setOnBackupListener(OnBackupListener onBackupListener) {
this.onBackupListener = onBackupListener;
}
public interface OnBackupListener {
public void onFinishExport(String error);
public void onFinishImport(String error);
}
}
Các bạn sẽ cần chú ý 2 điểm sau:
- Mình dùng AsyncTask cho việc import database vì quá trình này có thể mất một thời gian dài. Nó sẽ giúp hiển thị 1 dialog lên biểu thị đang xử lý làm người dùng yên tâm nếu không người dùng tưởng ứng dụng bị đơ.
- Trong quá trình copy, mình có gọi lệnh Note note = db.cursorToNote(cursor); lệnh cursorToNote trước đó nằm trong class DatabaseHelper và là private, giờ muốn gọi được các bạn sửa thành public nhé.
Giao diện danh sách file để import res/layout/list_backup_file.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/lv_backup"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
Một số thay đổi trong file string.xml
res/values/string.xml
<resources>
<string name="app_name">My Note</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string name="add">Add</string>
<string name="delete">Delete</string>
<string name="save">Save</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="backup_data">Backup data</string>
<string name="import_data">Import data</string>
<string name="backup_before_import">Do you want backup curren database before import an old databas?</string>
<string name="select_file">Select file</string>
<string name="backup_empty">Backup file is empty</string>
<!-- -->
<string name="title_note">Title note</string>
<string name="content">Content</string>
<string name="title_activity_note">Create Note</string>
</resources>
All rights reserved