안드로이드에서 기본 DB를 내장하기

개인적으로 노래방 검색 앱을 만들어 쓰고 있는데 데이터가 몇만 개가 되다보니 아무리 서버에서 JSON 형식으로 빠르게 준다고 해도 그걸 가지고 로컬에 DB를 구축하는 게 꽤나 오래 걸렸다. 그래서 기본 DB를 내장한 채 출시하고 업데이트만 새로 받도록 하면 어떨까 했다.

방법은 꽤나 간단한데 asset(혹은 res.raw)로 두고 DB를 열기 전 데이터베이스 디렉터리 안으로 복사를 해 주면 된다. dbHelper.get(Readable|Writable)Database()를 호출하기 전에는 DB를 열지 않으므로 db helper의 onCreate에서 미리 DB 파일이 존재하는지 확인한 후 없으면 파일을 통채로 복사해 주면 된다.

        public DbHelper(Context context) {
            super(context, DB_NAME, null, DB_VERSION);
            this.context = context;

            if (checkDbExists() == false) {
                try {
                    copyDatabase(DB_NAME);
                    Log.i("DB", "Copy initial database succeeded.");
                } catch (IOException e) {
                    Log.e("DB", "Failed to copy database");
                }
            }
        }

        private void copyDatabase(String filename) throws IOException {
            InputStream input = context.getAssets().open(DB_NAME);
            context.getDatabasePath(filename).getParentFile().mkdirs();
            String dbPath = context.getDatabasePath(filename).toString();
            OutputStream output;
            output = new FileOutputStream(dbPath);

            byte[] buffer = new byte[1024];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }

            output.flush();
            output.close();
            input.close();
        }

또 다른 문제가 하나 발생했는데 간편검색을 위한 컬럼을 추가하면서 초기 DB파일이 많이 바뀌었다. 신규 사용자는 역시 복사가 되겠지만 기존 사용자들은 DB가 이미 있기 때문에 파일을 복사하지 않을 것이고 몇만 개의 데이터에 대해 무거운 작업을 처리해야 했다. 다른 DB 파일에서 테이블 하나만 복사를 할 수 없나 싶다가 방법을 찾았다. attach 명령으로 다른 파일을 불러온 후 insert ~ select ~ from ~으로 데이터를 복사하면 금방 끝났다.
단, 그냥 insert를 하면 중복된 행이 발생하니 unique (col1, col2) on conflict update와 같은 스키마를 입혀 준 상태로 해야 한다.

여기서도 문제가 있는데 onUpgrade 메소드는 트랜잭션이 걸린 상태의 db를 인자로 전달해 준다. 하지만 attach 명령은 트랜잭션 중간엔 쓰지 못한다고 에러가 뜨는데 그래서 트랜잭션을 임시로 끝낸 후 작업을 한 뒤 다시 트랜잭션을 시작해 줘야 한다(사실 이 방법도 좋은 방법은 아니다. 트랜잭션이 왜 있는가).

                    db.execSQL(
                            "alter table " + TABLE_SONG + " add column " + COL_SIMPLIFIED +
                                    " not null default \"\";"
                    );

                    try {
                        // Temporarily end transaction to attach database;
                        db.setTransactionSuccessful();
                        db.endTransaction();

                        String dbName = "karaoke_orig";
                        copyDatabase(dbName);
                        db.execSQL(MessageFormat.format("attach \"{0}\" as orig",
                                context.getDatabasePath(dbName).toString()));
                        db.execSQL("insert into songs select * from orig.songs");
                        db.execSQL("detach orig");
                        Log.d("MIGRATE", "Copy simplified from asset");
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        db.beginTransaction();
                    }

Android 디버그 빌드시 앱 아이콘이나 이름 바꾸기

예전에 안드로이드 디버그 빌드시 스토어 버전과 충돌하지 않도록 버전네임과 패키지네임을 바꾸도록 설정하는 글을 쓴 적이 있다.
하지만 이런 방법을 써도 나중에 앱을 실행할 때 런처가 두 개나 존재하는데 이름도 똑같고 아이콘마저 똑같아서 어떤 게 디버그 버전인지 구분이 가지 않는 문제가 있었고 아이콘이든 이름이든 바꿀 수 있으면 좋겠다고 생각했었다.

예전엔 못 찾았지만 오늘 스택오버플로를 뒤져보다가 방법을 찾았는데 번역된 리소스를 넣을 때 values-ko 같은 다른 이름의 디렉터리를 만드는 것과 비슷하게 src 디렉터리 밑에 main 대신 debug를 만들어 똑같은 구조를 만들어 두면 디버그 빌드시 해당 디렉터리 안의 리소스를 사용하게 된다. 나는 새로 이미지를 만들기 귀찮아서 런처의 이름만 바꿨지만 리소스든 뭐든 얼마든지 바꿀 수 있으니 디버그모드에만 보이는 기능을 추가할 수도 있겠다.

screenshot_20161204-215933