因為在 Android Q (API 29) 後有 getExternalStoragePublicDirectory is deprecated 的狀況,所以最後終於東拼西湊出可以複製。
下列程式是用在 Fragment,如果是Android Q以上,會有檔案選取頁面選擇要儲存備份的檔案位置和自訂檔名,在還原時也可以自行選擇還原檔案。其他較低的Android 則在備份時直接以指定檔名儲存到 Download 資料夾,還原時也從 Download 資料夾讀取指定檔名。
下方式完整有關的程式碼
app.db 是 app 儲存 database 的檔名
backup.db 是儲存到 Download 資料夾的備份檔案檔名
使用在 Fragment Class 中
class FragmentMain : Fragment() {作為 dialog 建立時使用的 View
private var mView: View? = null
在 class 開始先定義 ActivityResultLaunchar<String!>
寫入的 Launchar,因為 onActivityResult() deprecated ,改成用 registerForActivityResult()
裡面的 it 是 android.net.Uri ,可以直接使用在 openOutputStream(Uri) 和 openInputStream(Uri)。
但是上方兩個 Open...Stream() 產生的 outputStream 和 InputStream 就不一定都可以直接使用在 copyTo()
context?.getDatabasePath(String) 可以直接取得 java.io.File,可以直接以 inputStream() 轉成 FileInputStream,在 copyTo 作為讀取沒問題,有 InputStream.copyTo(OutputStream, buffersize)。
在還原時寫入 app 的 database檔案也可以用 context?.getDatabasePath(String)?.outputStream() 取得 FileOutputStream ,直接用在 copyTo(),
因為 FileOutputStream 是由 OutputStream 擴充而來, FileInputStream 是由 InputStream 擴充的。
setContentFilterItems() 是重新將還原的資料庫內容讀取到 RecyclerView 的資料內,並更新 RecyclerView()
// 寫入的 Launchar,採用 CreateDocument()
private var mGetContent = registerForActivityResult(
ActivityResultContracts.CreateDocument()){ netUri: Uri! ->
val resolver = context?.contentResolver
resolver?.openOutputStream(netUri!!).use { inputStream: InputStream? ->
val inputStream = context?.getDatabasePath(
"app.db")?.inputStream()
inputStream?.copyTo(inputStream!!, DEFAULT_BUFFER_SIZE)
}
}
private var mGetContentOpenDoc = registerForActivityResult(
(ActivityResultContracts.OpenDocument())){ netUri:Uri! ->
val resolver = context?.contentResolver
resolver?.openInputStream(netUri!!).use { inputStream: InputStream? ->
val outputStream = context?.getDatabasePath(
"app.db")?.outputStream()
inputStream?.copyTo(outputStream!!, DEFAULT_BUFFER_SIZE)
setContentFilterItems()
}
}
把Fragment 的 View 存放到 mView,要用在產生 AlertDialog
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mView = view
}
處理備份和還原的主要函式
isBackup 決定是備份還是還原
fileName 是預設儲存備份檔案的檔名
上半部是無法使用 getExternalStoragePublicDirectory() 的時候執行
測試到 API 22 LOLLIPOP_MR1 也就是 Android 5.1 都還是可以正常運作
在使用內容是 OpenDocument() 的 ActivityResultLaunchar: mGetContentOpenDoc 時,launch() 要輸入允許讀取的檔案的MIME type,這裡是使用代表 Binary Data 的 application/octet-stream ,可以以陣列同時允許讀取多種類型檔案,MIME type 可以到 freeformatter.com 的 MIME Types Lists 或 Mozilla 的 MIME類別 查。
下半部是舊的 API 使用
Enviroment.getExternalStoragePublicDirectory() 和 context?.getDatabasePath(String) 取得 java.io.File,
可以直接用在 File.copyTo(File,overwrite:Boolean, buffersize),可是在還原時,copyTo 好像會延遲寫入,導致接下來的 setContentFilterItems() 更新 RecyclerView 時還不是新的資料,所以改成把 FIle 轉成 FileInptstream ,使用和前方 FileInputstream 的同樣方式,才能即時顯示新的資料。
private fun backupRestoreDB(isBackup:Boolean,fileName:String){
//輸出 database 檔案位置到log
Log.d("backdb", context?.getDatabasePath(
FilterDbHelper.DATABASE_NAME).toString())
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (isBackup) {
// CreateDocument 的 ActivityResultLauncher 是傳入預設檔名
mGetContent.launch(fileName)
} else {
mGetContentOpenDoc.launch(
arrayOf("application/octet-stream"))
}
} else {
var dlPath = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS)
var dbFFile = context?.getDatabasePath("app.db")
var newFile = File(dlPath.toString()+"/"+fileName)
if(isBackup) {
var cFilte = dbFFile?.copyTo(
newFile, true, DEFAULT_BUFFER_SIZE)
val builder = AlertDialog.Builder(context)
builder.setMessage(newFile.absolutePath).setNeutralButton(
R.string.yes, null).show()
}else{
if(newFile.exists()) {
var cFilte = newFile.inputStream().copyTo(
dbFFile!!.outputStream(), DEFAULT_BUFFER_SIZE)
Snackbar.make(
mView!!, "restored", Snackbar.LENGTH_LONG).setAction(
"Action", null).show()
setContentFilterItems()
} else {
Snackbar.make(mView!!, resources.getString(
R.string.file_not_exist,newFile.absolutePath), Snackbar.LENGTH_LONG).setAction(
"Action", null).show()
}
}
}
}
上方 R.string.file_not_exist 在 string.xml 中
<string name="file_not_exist">%s not exist.</string>
因為還原時可能會誤按還原按鈕,所以執行還原前,先顯示詢問是否確定要還原。
上方使用到 string.xml 的內容
private fun showRestoreComfirmDialog(){
val dialogClickListener =
DialogInterface.OnClickListener { dialog, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
backupRestoreDB(false, "backup.db")
}
DialogInterface.BUTTON_NEGATIVE -> {
}
}
}
val builder = AlertDialog.Builder(context)
builder.setMessage(R.string.confirm_restore_db).setPositiveButton(
R.string.yes, dialogClickListener)
.setNegativeButton(R.string.cancel, dialogClickListener).show()
}
<string name="yes">Yes</string>
<string name="cancel">Cancel</string>
使用方式
執行備份
backupRestoreDB(true,resources.getString("backup.db"))
執行還原前先詢問是否要還原
showRestoreComfirmDialog()
Class結束
}
參考資料:
https://blog.csdn.net/jingzz1/article/details/107338872
https://www.baeldung.com/kotlin/read-file
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-input-stream/copy-to.html
https://stackoverflow.com/questions/35528409/write-a-large-inputstream-to-file-in-kotlin/35529070
https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Basics_of_HTTP/MIME_types
https://www.freeformatter.com/mime-types-list.html
沒有留言:
張貼留言