안드로이드에서 기본 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

모바일 메신저 알림 지옥에서 벗어나는 방법

스마트폰이 퍼지고 메신저가 널리 사용 되면서부터 시작 된 고민이 있었는데 예전에도 글로 남긴 적이 있다(link).

그래서 몇 년 전부터 생각하고 있던 방법이 있는데 알림이 오기는 오되 같은 방에서 일정 시간 이내에 연속으로 오는 메시지에 대해서는 알림을 울리지 않는 것이다.
그렇게 하면 메시지를 놓치지 않을 수 있고 진동 폭풍에서 벗어날 수 있다. 하지만 어떤 메신저도 이런 기능을 구현하지 않고 있다. 1시간 알림 끄기 정도가 전부이다. 그걸 5분으로 줄이고 메시지가 올 때마다 자동으로 설정하게 하면 끝인데 왜 하질 않는지 모르겠다.

단체 채팅방의 경우 엄청 심한 문제인데 대부분의 채팅의 경우 한동안 조용하다가 대화가 시작되면 거의 초 단위로 메시지들이 온다. 이러면 한동안 가만히 있다가 알림이 왔을 때 내가 즉시 앱을 열지 않는 한 무한한 알림 지옥에 빠지게 된다. 그렇다고 알림을 끄면 대화가 언제 시작되는지도 모르고 하루 일과가 끝났을 때 우편함을 확인하듯이 이미 지나간 대화를 읽어야 하는데 페이스북이나 트위터, 혹은 블로그면 모를까 채팅에서 이러는 건 아무런 의미도 없기 때문에 소통을 거부하든지 알림 지옥에 들어가든지 하나의 선택을 해야만 한다.

요즘의 경우 이 문제점을 제작사들도 알고 있는지 1시간 뮤트 같은 기능을 제공하긴 하지만 결국은 내가 앱을 열고 뮤트를 설정해야 한다. 근본적인 문제는 아예 해결이 안 된 것이다.
내가 생각했던 방법은 일단 알림이 울리긴 하는데 일정 시간 내에 너무 많은 알림이 오면 자동으로 1시간 정도 뮤트를 설정해버리는 기능이었다. 구현하기 어려운 것도 아닌데 이게 되는 메신저를 본 적이 여태 없다는 게 굉장히 실망스러웠고 오늘 다시 검색을 해 봤다. 앱에서 지원을 안 하면 OS가 해 주면 되는 거 아닌가 했지만 안타깝게도 안드로이드에선 이런 기능이 없다. CM롬에서도 없다. 하지만 Xposed라는 게 있었다.

Xposed는 안드로이드에 이것저것 모듈을 붙여서 심하면 커스텀롬의 기능을 모두 붙일 수도 있는데 여기면 누군가가 만들었겠다 싶어서 찾아 봤더니 역시나 있었다. Less frequently notification이라는 물건인데 설정한 시간 단위 이내에 알림이 많이 오면(그 많다의 기준은 모르겠다. 설정도 없고..) 그 이후의 알림은 소리, 진동 중 원하는 것을 안 울리게 할 수 있다. GCM이 날아오고 앱이 잠들기 상태에서 깨어나 배터리를 먹는 건 어쩔 수 없지만 일단 사용자인 내가 스트레스를 덜 받을 수 있다는 점에 만족하기로 했다.

P.S: 폰에서 알림 소리와 진동이 안 나지만 일단 노티바에는 뜨기 때문에 Pebble에서는 그걸 인식하고 시계에 알림을 전송한다. 폰은 조용한데 손목에서는 계속 진동이 와서 귀찮아지긴 하지만 이건 이제 Pebble이 해결 할 문제지.. 알림이 올 때 그 알림이 진동을 울렸는지, 소리를 울렸는지 판단 할 수 있다면 시계에서도 화면만 띄우고 진동을 안 울리면 좋겠는데 바보같은 개발자가 노티바 내용과 진동을 따로 발생시킨다면?