본문 바로가기
개발/Android

[안드로이드 Android] Assets 에서 데이터베이스 를 비교 하고 복사하는 코드

by 언제나초심. 2019. 3. 12.
반응형

개요

Assets 에서 데이터베이스 를 비교 하고 복사하는 코드.


kotlin 으로 되어 있습니다. 필요해져서 만들게 된 코드 입니다. 


SQLiteAssetHelper 라는 라이브러리도 있는 듯 한데, 저 같은 경우는, 사실 이 아래에 적힌 것보다 더 많이 복잡한 루틴이 들어갔기 때문에, 직접 만들게 되었습니다. (ㅠㅜ)


참고하실 분을 위해서 적어둡니다. 


본문

(주의) 아래에 있는 코드는 필요한 부분만 남겨서 잘라낸 코드 입니다. 실제 동작시 빠져있는 부분이 있을 수 있습니다. 



import android.content.Context
import android.content.SharedPreferences
import android.database.sqlite.SQLiteDatabase
import android.preference.PreferenceManager
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

/**
 */
class AppDatabaseAssetsHandler (context: Context){
    // context 레퍼런스
    private val mContext = context
    // 앱에서 실행되는 Database File 의 이름
    private val mDatabaseName = "app_database.db"
    // 앱에서 실행되는 Database File 의 전체 경로
    private var mDatabasePath :String
    // 앱에서 실행되는 Database 의 버전의 설정값 이름
    private val versionPreferencesKey = "DB_CURRENT_VERSION"
    // assets 의 DB 파일의 경로
    private val assetDbFilePath = "db"
    // assets 의 DB 파일의 파일명
    private val assetDbFileName = "app_database.db"
    // assets 의 DB 파일의 버전을 담고 있는 파일
    private val assetDbVersionFileName = "app_database.db.version"


    init{
        mDatabasePath = context.getDatabasePath(mDatabaseName).path

        initialize()
    }

    /**
     * 데이터베이스 파일을 생성하고 업데이트 하는 메서드
     * 디비 파일이 없으면, 디비 파일을 복사해 온다.
     * 디비 버전을 체크해서, Assets 에 있는 DB 가 최신 것이라면 복사해온다.
     * @todo 최초 설치와 업데이트 설치에만 Assets 에서 복사해 오도록 변경할 필요 있음. (급하지는 않음. 지금도 속도는 충분)
     */
    private fun initialize(){
        // 설정 관리자
        val prefs = PreferenceManager.getDefaultSharedPreferences(mContext)

        // Local 데이터베이스가 있는지 확인하고, 분기를 나눔.
        if(!existsDatabaseLoaded(mContext)){
            // 보통 첫 설치 때의 동작. 속도가 느릴 수 있으므로, 최대한 간단히 동작하도록 한다.
            debug("[open] 데이터베이스가 없음")

            // assets 디비 파일이 있는지 체크하고, 있을 시에 복사함
            try {
                if (existsAssetsDatabase(mContext)) {
                    // 오래된 버전의 기기인 경우 databases 디렉토리를 생성 안 하는 버그가 있다. 이 경우를 대비해서 디렉토리를 미리 생성
                    File(mDatabasePath).also{
                        file -> file.parentFile.mkdirs()
                    }.createNewFile()

                    debug("[open] Assets 에서 데이터베이스 파일 복사 실행")
                    copyDatabaseFromAssets(mContext)
                    debug("[open] getAssetDbVersion")
                    val assetDbVersion = getAssetDbVersion(mContext)
                    debug("[open] setAppDbVersionPreference")
                    setAppDbVersionPreference(prefs, assetDbVersion)
                }
            } catch (e: Exception){
                debug("[open] Assets 에서 복사 실패 ",e)
            }

        } else {
            // 앱을 실행하거나, 업데이트 했을 때 등의 동작.
            debug("[open] 데이터베이스가 존재함")
            val localDbVersion = getAppDbVersionPreference(prefs)
            val assetDbVersion = getAssetDbVersion(mContext)

            debug("[open] 실행 초기 로컬 DB 버전 : ",localDbVersion)
            debug("[open] Assets 에 위치한 DB 버전 : ",assetDbVersion)

            // assets 의 버전이 높을 경우, assets 의 파일을 다운로드 함.
            if(localDbVersion < assetDbVersion){
                try {
                    // assets 디비 파일이 있는지 체크하고, 있을 시에 복사함
                    if(existsAssetsDatabase(mContext)) {
                        debug("[open] Assets 에서 데이터베이스 파일 복사 실행")
                        copyDatabaseFromAssets(mContext)
                        setAppDbVersionPreference(prefs,assetDbVersion)
                    }
                } catch (e: Exception) {
                    debug("[open] Assets 에서 복사 실패  ",e)
                }
            }
        }
    }

    /**
     * 데이터베이스 경로를 리턴.
     */
    fun getDatabasePath() : String{
        return mDatabasePath
    }

    /**
     * Preferences 에서 App DB 버전 값을 가져옴.
     * PreferenceManager 를 여러번 이용할 경우에는
     * getAppDbVersionPreference(prefs : SharedPreferences) 를 이용하기를 권장함.
     */
    @Suppress("unused")
    private fun getAppDbVersionPreference() : Int{
        val prefs = PreferenceManager.getDefaultSharedPreferences(mContext)
        return getAppDbVersionPreference(prefs)
    }

    /**
     * Preferences 에서 App DB 버전 값을 가져옴
     */
    @Suppress("unused")
    private fun getAppDbVersionPreference(prefs : SharedPreferences) : Int{
        return prefs.getInt(versionPreferencesKey,0)
    }

    /**
     * 버전 정보값을 Preferences 에 저장 하는 메서드.
     */
    private fun setAppDbVersionPreference(prefs : SharedPreferences, version:Int){
        val editor = prefs.edit()
        editor.putInt(versionPreferencesKey,version)
        editor.apply()
    }

    /**
     * Assets 에 있는 Version 이 담긴 text 파일에서 버전 정보를 읽어온다.
     * 에러가 있을 경우 그저 0 을 반환한다.
     */
    private fun getAssetDbVersion(context:Context):Int{
        return try {
            Integer.parseInt(readTextFromAssets(context,"$assetDbFilePath/$assetDbVersionFileName"))
        } catch (e: Exception) {
            0
        }
    }

    /**
     * File 에서 version 정보를 가져오는 메서드
     * 연결을 해야하기 때문에 속도가 느릴 수도 있음...
     */
    @Suppress("unused")
    private fun getVersionFromDatabaseFile(file: File): Int{
        // 파일의 존재 유무를 먼저 체크한다. 그런데 이미 file 개체로 넘어왔으니 있을 것 같은데..
        return if(file.exists())
        {
            val db:SQLiteDatabase = SQLiteDatabase.openOrCreateDatabase(file,null)
            val version = db.version
            db.close()
            version
        } else {
            0
        }
    }

    /**
     * 현재 로드된 데이터베이스 가 존재하는지 여부 확인.
     * 특별한 경우가 없다면, 최초 1회 확인만 하게 될 듯.
     * 인수로 Context 를 넘겼는데, 혹시 모르니 불안해서 넣은 것임. 그대로 둘 것.
     * (데이터베이스명은 문자열로 들어갔고 변경여지가 없으나, Context 는 때에 따라서는 바뀌는 변수가 될 수 있음)
     * 데이터베이스를 open 해서 try 로 확인하는 방법이 좀 더 유용하지만,
     * 단순히 file 의 존재만 확인 하는 것으로 하기로 함.
     */
    private fun existsDatabaseLoaded(context:Context): Boolean{
        val dbFile = context.getDatabasePath(mDatabaseName)
        //debug("[existsDatabaseLoaded]",dbFile.path)
        return dbFile.exists()
    }

    /**
     * Assets 에 데이터베이스 파일이 존재하는지 유무.
     */
    private fun existsAssetsDatabase(context:Context):Boolean {
        return existsAssetFile(context,assetDbFilePath,assetDbFileName)
    }

    /**
     * Assets 에 있는 DB 파일을 local DB 에 복사.
     */
    @Throws(IOException::class)
    private fun copyDatabaseFromAssets(context:Context){
        copyFromAssets(context, "$assetDbFilePath/$assetDbFileName", mDatabasePath)
    }

    /**
     * Assets 에 파일이 있는지 확인하는 메서드.
     * assets 에서 한 단계 밑의 폴더 내의 파일을 확인할 수 있다.
     * 두 단계 이상으로는 구현 안 함. 오히려 비효율적일 수 있어서.
     */
    private fun existsAssetFile(context:Context,path:String,fileName:String):Boolean {
        return context.assets.list(path).toList().contains(fileName)
    }

    /**
     *
     */
    private fun readTextFromAssets(context:Context, assetURI:String) : String{
        return context.assets.open(assetURI).bufferedReader().use {
            it.readText()
        }
    }
    /**
     * Assets 에서 파일을 복사하는 메서드.
     * 일반적으로 사용 가능함.
     */
    @Throws(IOException::class)
    private fun copyFromAssets(context:Context, assetURI : String, toPath:String){
        FileOutputStream(toPath).use { out ->
            context.assets.open(assetURI).use {
                it.copyTo(out)
            }
        }
    }

    /**
     * 디버깅 메서드
     * @param msg 메시지
     */
    @Suppress("unused")
    private fun debug(msg: String, msg2 : Any = "") {
        @Suppress("ConstantConditionIf")
        if (isDebug) {
            Log.d(TAG, "$msg $msg2")
        }
    }

    companion object {
        private const val TAG = "[AppDatabaseAssetsHandler]"
        private const val isDebug = false
    }
}



변수 값 설명

mDatabaseName : 앱 내부에 있게될 db 파일명

versionPreferencesKey : 현재의 디비 버전 값을 넣어둘 Preference 키

assetDbFilePath : assets 에 넣어둘 때, 디렉토리 경로. (폴더 한 단계로만 해주세요... 저는 db 폴더를 만들고 넣었습니다)

assetDbFileName : assets 에 넣어둘 db 파일명

assetDbVersionFileName : assets 에 넣어둘 db 파일의 버전 값 (단순히 숫자만 넣어주면 됨)


이 값들을 수정하면 됩니다. 


향후에는 assets 에 db 파일을 수정해서 넣고, version 도 텍스트 문서에 적어서 변경해주고 넣어주면, 이것으로 비교를 합니다. 


(assets 의 db 파일을 직접 열어서 그 안의 version 값을 비교하고 싶은데, 오히려 비효율적일 것 같아서 그렇게 안 했습니다.. 너무 번거로워져요...)


assets

- db 

     - blah.db : 디비 파일.

     - blah.version : 버전 값을 숫자로 기입.




호출

중요한 것은 호출입니다. 설명이 어려울 수 있는데....


A 라는 싱글턴 클래스를 만들고. (저는 Object 로 만들었습니다.. 귀찮아서.. )


MainActivity 의 onCreate 에서 A 클래스의 load 를 호출합니다. 


A 클래스의 load 라는 메서드를 만들고 그 안에서 아래와 같은 구문을 넣었습니다.


appDatabasePath 는 A 클래스의 멤버 변수 입니다. 



    // appDatabasePath 는 만든 멤버변수
    if(appDatabasePath==""){
        doAsync {
            //디비 연결 및 생성과 Assets 을 통한 업데이트
            appDatabaseHandler = AppDatabaseAssetsHandler (context.applicationContext)
            // 데이터베이스의 경로만 갖는다.
            appDatabasePath = appDatabaseHandler.getDatabasePath()

            }
        }


appDatabasePath 라는 멤버 변수를 하나 만들어서, 여기에 값이 아직 들어오기 전 일 때에만 동작하도록 처리를 하였습니다. 


이렇게 하면, 앱 실행 처음 한번에만 동작되게 됩니다. 



참고

doAsync 는 'anko' 의 '라이브러리'를 사용한 것입니다... 이것까지 설명하려면.. doAsync 를 사용 안 하셔도 됩니다. 이것은 비동기호출을 하기 위한 부분이라서, 있으면 좋고 없어도 되는 부분입니다. (속도 차이는 분명합니다..)


doAsync 에 대해서는 아래를 참고해주세요. (구글링)

https://www.google.com/search?q=android+doasync 


doAsync 를 사용안하시고, 일반적으로 사용하는 AsyncTask 를 이용하셔도 됩니다. 




반응형