2014년 11월 26일 수요일

[Android]안드로이드에서 진동 구현

안드로이드에서 알람이나 게임의 재미를 높이기위해서 진동을 활용할 수 있다. 안드로이드에서 진동 구현은 iOS보다 복잡하다. 먼저 메니페스토 파일에서 Vibrate에 관한 권한을 부여한다.




 Vibrate 권한을 구현한 다음에는 간단한 코드 몇 줄이 필요하다. 먼저 Vibrate에 관한 시스템 서비스에관한 객체를 부여받는다. 그리고 Vibrator 객체를 이용하여 진동을 구현한다.

Vibrator tVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);


long[] vibratePattern = {100, 100, 300};


tVibrator.vibrate(300);
tVibrator.vibrate(vibratePattern, -1);


 vibrate는 밀리초 단위로 설정이 가능하다. 그리고 long Array를 사용하여 진동 패턴을 줄 수 있다. 그리고 반복 회수를 지정하여 다양한 진동 패턴을 만들 수 있다. 반복 회수를 '0'으로 하면 무한 반복이다. 반복을 원치 않으면 '-1'로 설정해주면 된다. 

[안드로이드] 진동 울리기 ( 지정시간 진동과 패턴 진동)

팁스소프트에서 제공하는 프로그래밍과 관련된 자료나 정보들을 무단으로 복제하거나 게재하는 행위는
상호간의 신뢰를 무너뜨리는 행위이며, 법적인 문제를 야기할 수 있으므로 각별한 주의를 당부드립니다.
* 팁스소프트 저작권 정책 보기 -  http://www.tipssoft.com/bulletin/tb.php/FAQ/637
 
이 자료들은 팁스소프트에서 제공하는 [ 알짜배기 ] 프로그램을 이용하면 더 편리하게 볼수 있습니다.
* 알짜배기 프로그램 받기 -  http://www.tipssoft.com/bulletin/tb.php/QnA/8406
 
* 관리자의 Tipssoft 이야기를 들어보세요 ( 트위터 ID : tipssoft ) 
 
* 안드로이드 강좌 목록 - http://www.tipssoft.com/bulletin/tb.php/old_bbs/501 
    
 
휴대가 가능한 대부분의 안드로이드용 기기들은 공공장소나 조용한 곳에서 기기를 사용할 때 불편하지
않도록 매너모드를 제공하고 있습니다. 매너모드 상태의 기기는 사용자에게 무언가를 알릴때 소리를
울리는 대신 진동을 발생시켜서 알려줍니다. 매너모드뿐만 아니라 게임이나 다른 어플리케이션에서도
좀더 강력한 효과를 발생시키기 위해서 진동을 사용하기도 합니다.
 
이번 강좌에서는 알림, 효과용 등 여러가지 용도로 사용되는 진동에 대하여 알아보도록 하겠습니다.
 
 
1. 기능 구현하기 
 
    진동을 울리는 기능은 시스템에서 서비스하는 일이기때문에 먼저 AndroidManifest.xml
    파일에 아래의 코드를 추가하여 이 어플리케이션이 해당 기능을 사용할 수 있도록 권한을
    부여해주어야 합니다.
 
    <manifest>
        // ... 생략 ...
        <application>
            // ... 생략 ...        </application>

        <uses-permission android:name="android.permission.VIBRATE" />

    </manifest>
 
    진동 울리기 기능을 사용하려면 먼저 시스템서비스로부터 Vibrator 객체를 얻어야하며 Vibrator
    클래스의 vibrate 메소드를 이용하여 진동을 쉽게 발생시키고, cancel 메소드로 울리고 있는 진동을
    멈출 수 있습니다.
 
    진동을 발생시키는 vibrate 메소드는 다음과 같은 두개의 메소드로 오버로딩되어 있습니다.
 
    public void vibrate (long milliseconds)             // 지정시간 동안 진동을 울린다.    public void vibrate (long[] pattern, int repeat)   // 배열에 지정한 패턴대로 진동을 울린다.
 
    인자가 하나인 vibrate 메소드는 지정한 밀리초만큼 진동을 발생시키고, 두번째 vibrate 메소드는
    pattern 인자에 입력한 패턴대로 진동을 발생시키는 메소드입니다.
 
    패턴 진동을 지원하는 vibrate 메소드를 사용하려면 배열을 구성할 때 다음과 같은 규칙을 고려하여야
    합니다.  
 
    
 
    위의 그림처럼 구성되는 배열의 짝수 index 에는 대기 시간을 지정하고, 홀수 index 에는 진동 시간을
    지정해주어야합니다.
 
    또 두번째 매개 변수인 repeat 에는 반복을 시작할 index 를 명시해줍니다. 이 때 주의해야할 점은
    시작되는 index 부터 대기 시간이 적용되기 때문에 울림시간과 대기시간이 생각한것과 반대로 적용될
    수 있다는 것입니다. 예를들어 위의 그림과 같은 배열 구성이 있는 경우 repeat 에 1 을 명시하면 아래의
    그림처럼 기존의 진동 패턴과 완전히 달라지게 됩니다.
 
    
   
    만약 구성한 배열 그대로 반복하여 진동을 울리고 싶다면 repeat 에 0 을 명시하시고, 한번만 진동을
    울리려면 -1 을 명시하시면 됩니다.
 
    아래의 코드는 두가지 vibrate 메소드를 사용하여 진동을 울리는 예제입니다.
 
 
    public class TestVibratorActivity extends Activity implements OnClickListener
    {
        Vibrator m_vibrator = null;

        public void onCreate(Bundle savedInstanceState)
        {
            // ... 생략 ...             

            // 시스템서비스로부터 진동을 울릴 수 있는 Vibrator 객체를 얻는다.            m_vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        }
       
        public void onClick(View view)
        {
            int id = view.getId();
           
            if(id == R.id.once_btn) {  // 지정된 시간만큼 진동이 울려야 하는 경우
                // 에디트텍스트를 얻어서 입력된 수치값을 구한다.
                EditText edit = (EditText) findViewById(R.id.num_edit);
                String str = edit.getText().toString();
               
                // 지정된 수치값만큼 진동을 발생시킨다.                m_vibrator.vibrate(Integer.parseInt(str));
               
            } else if(id == R.id.go_btn) {  // 지정된 패턴대로 진동이 울려야 하는 경우
                // 진동 패턴을 구성한다.
                long[] pattern = {50, 100, 100, 200, 100, 300};

                // 구성한 패턴대로 진동을 발생시킨다.
                // 첫 진동패턴은 50ms 을 대기하면서 시작하고,
                // 두번째 진동패턴부터는 100ms 을 대기하면서 진동이 시작된다.                 m_vibrator.vibrate(pattern, 1);
            } else if(id == R.id.cancel_btn) {  // 진동이 중지되어야 하는 경우
                // 진동을 중지시킨다.
                m_vibrator.cancel();
            }
        }
    }
 
 
2. 실행 화면
 
    

2014년 11월 25일 화요일

GCM 진동 / 화면 켜짐 적용

소스 수정 
    @Override
    protected void onMessage(Context context, Intent intent) {
        Log.i(TAG, "Received message");  
        Log.i(TAG, "Received message context " +  intent.getExtras().getString("message"));
        //String message = getString(R.string.gcm_message);
        Vibrator vibrator = (Vibrator)getSystemService(context.VIBRATOR_SERVICE);
        vibrator.vibrate(1000);
        
        WakeUpScreen.acquire(getApplicationContext(), 10000); 
        
        String message = intent.getExtras().getString("message");
        // notifies user
        generateNotification(context, message);
       
    }

WakeUpScreen  CLASS 추가


import android.content.Context;
import android.os.PowerManager;

/**
 * 스크린을 ON한다. 젤리빈 4.2부터는 getWindows() 권장
 * @author IKCHOI
 *
 */
public class WakeUpScreen {

    private static PowerManager.WakeLock wakeLock;

    /**
     * timeout을 설정하면, 자동으로 릴리즈됨
     * @param context
     * @param timeout
     */
    public static void acquire(Context context, long timeout) {

        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        wakeLock = pm.newWakeLock(
                PowerManager.ACQUIRE_CAUSES_WAKEUP  |
                PowerManager.FULL_WAKE_LOCK         |
                PowerManager.ON_AFTER_RELEASE
                , context.getClass().getName());

        if(timeout > 0)
            wakeLock.acquire(timeout);
        else
            wakeLock.acquire();

    }

    /**
     * 이 메소드를 사용하면, 반드시 release를 해줘야 함
     * @param context
     */
    public static void acquire(Context context) {
        acquire(context, 0);
    }

    public static void release() {
        if (wakeLock.isHeld())
            wakeLock.release();
    }
}

Manifest 추가
    <uses-permission android:name="android.permission.VIBRATE"/>
    <!-- Keeps the processor from sleeping when a message is received. -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />


참고    http://nonstop.pe.kr/android/1612
참고 2  http://www.androidside.com/bbs/board.php?bo_table=b49&wr_id=107992

[Android] SQLite활용 : 서버에 있는 데이터베이스(DB) 내려받아 설치하기 [출처] [Android] SQLite활용 : 서버에 있는 데이터베이스(DB) 내려받아 설치하기|작성자 LifeClue


펌위치 : http://blog.naver.com/legendx/40153264388

내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.
서버와 DB를 활용한 원격 판올림은 서버에 SQLite 데이터베이스(이하 DB)를 올려 놓은 후 앱에서 다운로드 받아 새로운 DB로 갱신하여 앱을 판올림하는 것이다.
이는 안드로이드보다는 아이폰에 더 필요한 사항으로, 검수가 약 일주일이 걸리는 현실을 고려해 최근 여러 앱에 도입되는 추세다. (물론 이 전에도 지속적으로 도입되고 있었다. 예로 버스, 지하철 앱)

다행인 것은 안드로이드도, iOS도 SQLite를 지원한다는 것이다.
그러므로 우리는 이미지가 들어있지 않는 한 하나의 DB를 가지고 두 플랫폼을 모두 지원할 수 있다.
(하지만 본인 회사에서 서비스하는 앱은 이미지가 첨부되기 때문에 DB를 따로 운영한다 -_-; 무결성은 전적으로 사람에게로...;;;)
(현재 두 플랫폼 레이아웃에 맞춰봤을 때 더 큰 이미지를 탑재해 하나의 DB로 관리하고 있다. 아이폰 또는 안드로이드(xhdpi) 이미지를 탑재해서 둘 중 하나의 플랫폼에서 크기를 조절하여 쓰는 것도 한 방법이다.)

웹에서 찾아보면 "SQLite 활용하기", "DB 서버에서 다운받아 설치하기", "SQLite 테이블 생성" 등을 볼 수 있다. 본 포스트는 이러한 내용들을 통합한 것이라고 보면 된다. 참고로 본 포스트에서는 테이블 또는 레코드의 생성(테이블), 삭제, 수정, 삽입(레코드) 등은 다루지 않는다.

본 포스트에서 설명할 "서버에 업로드된 DB를 단말에서 받아 설치"하는 내용은 형규님의 포스트를 참고하였다.

본 포스트를 참고하기 위해서는 DB에 대한 기본지식, SQL문의 작성법, Java 네트워크 프로그래밍 등이 필요하다.

DB를 이용한 판올림의 과정은 다음과 같다.
(최초)DB생성 > DB 입력 > 서버에 DB 업로드 > 단말에 다운로드 > 단말에 설치 > 단말에서 활용
DB 생성은 최초 한 번만 해주면 되며, 이후에는 입력부터 시작해 업로드로 진행된다.

/* DB 생성, 입력 */
DB 생성은 SQLite를 위한 툴을 이용하면 한결 간편하다.
특히 Firefox의 부가기능인 SQLite Manager를 이용하면 쉽게 DB를 관리할 수 있다.
SQLite Manager로 검색하면 사용법등이 잘 나와있는 블로그들이 많다.
물병자리 시대의 여명 블로그 : SQLite Manager 설치하기

SQLite Manager에서 DB를 생성하면 DB 파일을 저장할 곳을 묻는데, 해당 위치에 가 보면 DB명으로 sqlite파일이 생성되어 있을 것이다.

/* 서버에 DB 업로드 */
DB가 생성되었다면 서버에 자리 한 켠을 마련하여 FTP등으로 올려놓자.

/* 단말에 다운로드 */
단말에서 DB를 내려받을지를 확인하는 경우는 2가지다.
1. 받아놓은 DB파일이 없을 때 (새로 설치 과정을 따르게 된다.)
2. 서버 파일이 갱신됐을 때 (판올림 과정을 따르게 된다.)

먼저 받아놓은 파일이 있는지 검색한다.
// 자주색으로 된 부분은 자신의 기호에 맞게 변경하여 사용하도록 한다.
File dbFile = new File(
Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite");
if (dbFile.exists()) {
    // 파일이 있을 경우 처리
} else {
    // 파일이 없다면 최초 DB 설치 과정으로 진행
    showDialog(DIALOG_DB_FIRST);
}

Environment.getDataDirectory().getAbsolutePath() 는 앱에 관련된 파일을 저장할 수 있는 공간의 경로를 받아온다.
위 경로 + data폴더에 파일을 저장할 수 있다.
그 중 자신이 쓸 것은 자신의 패키지 경로이므로 getPackageName()을 붙여준다.
마지막으로 DB 파일명을 붙인다.
(잘 이해가 안되시는 분은 mydb.sqlite 부분만 자신이 원하는 파일명으로 고쳐서 사용하시면 된다.)
DataDirectory를 사용하는 이유는 앱이 지워질 때 같이 지워질 수 있도록 하기 위함이다.
SD카드에 저장할 경우 사용자가 앱을 지워도 DB가 남아 기기 저장공간이 지저분해 질 수 있다.

- SD카드에 설치한 경우, 앱을 삭제했을 때 DB파일이 남는다. 외부에서 접근이 쉽다. 용량 확보가 용이하다.
- Data영역에 설치한 경우, 응용프로그램 관리에서 해당 앱 정보를 봤을 때 데이터에 있는 용량이 바로 이 DB 파일 용량이며, 데이터 지우기를 눌렀을 경우 DB파일을 지울 수 있고 앱을 삭제했을 경우 함께 삭제된다.

되도록 모든 문자열은 상수로 정하여 사용하는 것이 좋다. DB 경로는 DB를 열 때마다 사용된다.
(이 말인즉슨, Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite 과 같은 DB 경로는 여러 곳에서 쓰일 수 있기 때문에 매번 저 긴 코드를 붙여넣기 보다는 상수로 만들어 사용하는 것이 훨씬 용이하고 관리도 편하다는 것이다.)

다시 코드로 돌아가서 showDialog(DIALOG_DB_FIRST); 가 실행되면 현재 설치된 DB가 없어 설치를 진행해야 한다는 AlertDialog가 노출되고, 확인을 누르면 새로 다운로드받아 설치하게 된다.
(DIALOG_DB_FIRST는 상수다.)
*showDialog 참고
랩하는 프로그래머님 : [안드로이드] 다이얼로그 생성하기

그렇다면 DB 갱신은 어떻게 하는지 알아보자.

// 아래 코드는 위의 받아놓은 파일이 있는지 검색하는 코드를 확장한 것으로,
// 파일이 있을 경우를 처리하는 코드와 파일이 없을 경우를 처리하는 코드가 추가되었다.
/* checkDB() */
{
    File dbFile = new File(K.getDbAbsolutePath(this));
    if (dbFile.exists()) {

        // 파일이 있을 경우, DB의 상태를 확인한다. SELECT문을 날려서 데이터가 날라오지 않는다면 DB파일에 이상이 생긴 것이므로 다시 다운로드 받는다.
         SQLiteDatabase db = SQLiteDatabase.openDatabase(K.getDbAbsolutePath(SplashActivity.this)null, SQLiteDatabase.OPEN_READONLY | SQLiteDatabase.NO_LOCALIZED_COLLATORS);
        Cursor cursor = db.rawQuery("SELECT * FROM tablename LIMIT 1, 10"null);
        // SELECT문이 레코드를 못찾을 경우 count는 -1이다.        if (!db.isOpen() || cursor.getCount() <= 0) { 
            // DB가 비정상이라면 새로운 DB를 받기 전 사용자에게 대화상자로 알린다.
            handler.sendEmptyMessage(WHAT_DB_RECOVERY);
            return;
        } else {

            // 네트워크 처리를 위해 Thread 생성
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() { 
                    try {
                        // 서버의 DB 경로로 URL 객체를 생성한다.
    // K.DB_URL = "http://xxxxxxx.xxx/xxxxxx/db.sqlite"
                        URL url = new URL(K.DB_URL);
                        // 해당 URL로 연결한다.
                        URLConnection conn = url.openConnection();
                        // 해당 경로에 위치한 대상의 가장 최근에 수정된 날짜를 받아온다.(Millisecond)
                        // lastModified 변수는 long 형태로 선언되어야 한다. 클래스 멤버다.
                        lastModified = conn.getLastModified();
                        // 받아온 수가 0보다 크면 제대로 연결됐으므로 정상처리한다.
                        if (lastModified > 0) { 

                            // 단말기 내에 저장된 DB의 최신 변경 날짜를 조회하기 위해 생성한다.

                            // SharedPreferences 에 대해서는 다음을 참고하자.
                            // [Android] 상태 저장하기 (savedInstanceState, SharedPreference)

                            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(SplashActivity.this);
                            // 저장된 DB의 수정 날짜를 조회해 서버의 수정 날짜와 비교한다.
                            if (lastModified > pref.getLong("savedLatest", 0)) {
                                handler.sendEmptyMessage(WHAT_DB_UPDATE);
                            // 서버 DB가 갱신되지 않았다면 메인화면으로 넘어간다. (앱 바로 사용)                            } else {
                                handler.sendEmptyMessage(WHAT_DB_SKIP);
                            }

                        } else {
                            // 아니면 (lastModified가 0이면) 오류를 알린다.
                            handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                        }
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                        handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                    } catch (IOException e) {
                        e.printStackTrace();
                        handler.sendEmptyMessageDelayed(WHAT_DB_CHECK_ERROR, 1000);
                    }
                }
            });
            thread.start();
        }
        // 사용한 DB와 커서를 닫아준다.
        if (cursor != null) { 
            cursor.close();
        }
        if (db != null) { 
            db.close();
        }
    // 파일이 없을 경우 DB 최초 설치 과정으로 진행한다.
    } else {
        showDialog(DIALOG_DB_FIRST);
    }
}


handler가 호출하는 메서드에 들어가는 WHAT_DB_SKIP 이나 WHAT_DB_UPDATE같은 값들은 상수이며, 개발자가 임의의로 정하여 사용하면 된다.

본인의 경우, WHAT_DB_CHECK_ERROR일 때에는 그냥 정상 진행하는 것으로 처리하였다.
이는 이미 앞서 DB의 존재유무를 파악했기 때문에 서버에 접속할 수 없더라도 (만약 서버에 DB파일이 갱신되었다 하더라도) 이전 버전의 DB로 앱을 사용할 수 있도록 한 것이다.

DB가 갱신되었는지 비교 판단하는 방법으로 본인은 SharedPreferences(이하 SP)를 사용하였다.
일단 처음 설치시에는 DB에 대한 아무런 정보가 없기에 SP에 아무런 내용도 기록되어 있지 않겠지만,
일단 DB를 한 번이라도 설치했다면 DB의 수정된 날짜를 SP에 기록한 후 추후 서버의 수정된 날짜와 비교하여 업데이트를 판단하는 것이다.
그래서 위의 checkDB()가 끝나고 핸들러가 메세지를 받으면 각 메세지에 담겨진 what 값에 따라
대화상자를 띄우게 된다. (업데이트를 해야 하는지, 새로 설치해야 하는지)

이제 DB를 본격적으로 다운로드 받는 방법에 대해 알아보자.
위 소스를 보면 "처음 설치 과정"과 "업데이트", "복구" 과정이 있는데,
이 들의 차이는 간단하다.
다운로드 받기 전 나타나는 메세지에 차이가 있을 뿐이다.
1. 처음 설치한 경우
현재 저장된 DB가 없습니다.
앱을 사용하기 위해서 DB를 설치해야 합니다.
지금 설치하시겠습니까?
(본 과정은 네트워크를 사용하므로 와이파이 환경에서 진행하시길 권장합니다.)
[다운로드]    [종료]
2. DB 갱신

DB가 업데이트 되었습니다.

이전 버전 : 2012년 2월 18일 18시 16분
최신 버전 : 2012년 2월 22일 11시 43분
[다운로드]    [나중에]


3. DB 복구

DB가 손상되었습니다.
앱을 사용하기 위해서 DB를 복구해야 합니다.
지금 복구하시겠습니까?
(본 과정은 네트워크를 사용하므로 와이파이 환경에서 진행하시길 권장합니다.)
[다운로드]    [종료]


그리고 다운로드를 누르면 같은 메서드로 진입한다. 종료를 누르면 앱이 종료된다.
(대화상자에서 [다운로드] 버튼을 누르면 아래 메서드로 진입하도록 작성하면 된다.)
그럼 이제 DB를 내려받아보자.
/* downloadDB() */
 {
    // 내려받기 진행상황을 보여주기 위한 ProgressDialog를 띄운다.
    showDialog(DIALOG_DB_DOWNLOAD);
    Thread thread = new Thread(new Runnable() {
        
        @Override
        public void run() {
            InputStream is = null;
            FileOutputStream fos = null;
            BufferedOutputStream bos = null;
            URL url;
            URLConnection conn;
            try {
                // 서버의 DB 경로로 URL 객체를 생성한다.
                url = new URL(K.DB_URL);
                // 해당 URL로 연결한다.
                conn = url.openConnection();
                // 서버에 있는 DB 파일의 용량을 받아온다.
                int lengthOnServer = conn.getContentLength();
                // 용량이 0보다 크면 제대로 연결되었다고 판단하고 정상 진행한다.
                if (lengthOnServer > 0) {
                    dbDialog.setMax(lengthOnServer);
                } else {
                    // 용량이 0보다 크지 않을 경우 오류임을 표시한다.
                    handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
                    return;
                }
                if (lastModified == 0)
                    lastModified = conn.getLastModified();
                }
                // 다운로드 받은 파일을 시스템에 저장하기 위해 InputStream을 얻는다.
                is = conn.getInputStream();
                
                // DB 파일을 저장할 경로로 File 객체를 생성한다. (폴더 경로만)
                // 본인의 경우 K.getDbDirPath()를 호출하면 아래 경로를 반환하도록 작성하였다.
                // Environment.getDataDirectory().getAbsolutePath() + "/data/" + getPackageName() + "/mydb.sqlite"
                File dir = new File(K.getDbDirPath(Activity.this));
                // 만약 폴더가 없다면 생성한다.
                if (!dir.exists()) {
                    dir.mkdir();
                }
                // 폴더경로에 파일명까지 붙인 경로로 File 객체를 생성한다.
                File target = new File(K.getDbAbsolutePath(Activity.this));
                // 파일이 존재한다면 삭제한다.
                if (target.exists()) {
                    target.delete();
                }
                // 그리고 새로운 파일을 생성한다.
                target.createNewFile();
                
                // 아래는 생성한 파일에 서버의 DB를 쓰는 과정이다.
                fos = new FileOutputStream(target);
                bos = new BufferedOutputStream(fos);
                int bufferLength = 0;
                <font color="#009e25">// 다운로드가 진행되는동안 다운로드된 크기를 축적하는 맴버 변수다.</font>
                totalLength = 0;
                // 버퍼 크기가 클 수록 다운로드는 빨라진다. 하지만 메모리를 많이 잡고 있다는 것을 명심하자.
                byte[] buffer = new byte[1024];
                while((bufferLength = is.read(buffer)) > 0) {
                    // bos.write()로 파일을 쓸 수 있다.
                    bos.write(buffer, 0, bufferLength);
// 총 받은 양을 기록한다.
                    totalLength += bufferLength;
                    runOnUiThread(new Runnable() {
                        
                        @Override
                        public void run() {
                            // 진행 상태 대화창에 진행량을 증가시킨다.
                            dbDialog. setProgress(totalLength);
                        }
                    });
                    // sleep()없이 진행해보시면 아시겠지만, 없을 때 속도는 빠를지라도 사용자에게 명시적으로 보여주기 힘든 상황이 발생한다. 진행 과정이 부드럽게 보여지지 않는다. 하지만 없으면 다운로드가 빨라진다.
                    Thread.sleep(1);
                }

                // 내려받은 용량과 서버에 있는 DB 파일의 용량이 같지 않으면 오류를 표시한다.
                if (totalLength != lengthOnServer) {
                    handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
                    return;
                }

                // 마지막으로 100% 달성을 보여주기 위해 진행상태를 최대치로 입력한다.
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        dbDialog.setProgress(dbDialog.getMax());
                    }
                });

                // 다운로드가 완료된 후 행동을 취하기 위해 Handler에 Message를 전달한다.
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOADED, 1000);
            } catch (MalformedURLException e) {
                e.printStackTrace();
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
            } catch (IOException e) {
                e.printStackTrace();
                handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 최종적으로 사용했던 Stream들을 정리한다.
                try {
                    if (bos != null) bos.close();
                    if (fos != null) fos.close();
                    if (is != null) is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    thread.start();
}

다음 소스를 같이 살펴보도록 하자.

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // 진행 상태 대화창에 진행량을 증가시킨다.
        dbDialog.setProgress(totalLength);
    }
});
runOnUiThread는 UI작업을 할 때 사용하는 메서드다. 이 메서드를 UIThread가 아닌 Thread에서 호출하면 해당 작업을 UI Thread의 event queue에 집어넣는다.
* 자세한 Thread관련 내용은 다음 블로그를 참고하면 많은 도움이 될 듯 싶다.

하지만 아직까지 밝혀지지 않은 미스테리가 있다. runOnUiThread()를 사용하든, Handler의 post()를 사용하든 ProgressDialog의 ProgressBar와 Progress값이 서로 일치하지 않는다는 것이다. Bar는 덜 찼는데 값은 100%라거나, 반대의 경우가 생기기도 한다.
해서 본인이 생각한 방법이 바로 Thread.sleep(1);인 것이다. 주기적으로 쉬어주면 진행상태도 자연스럽고, 위와 같은 미스테리도 어느정도 해결된다.

진행 중 인터넷이 끊어지거나 용량이 상이한 경우
handler.sendEmptyMessageDelayed(WHAT_DB_DOWNLOAD_ERROR, 1000);
가 호출되는데, 이 때 오류를 알리는 AlertDialog를 띄우고 재시도를 하거나 종료할 수 있도록 하였다.

DB 다운로드가 끝났으면, 다운로드 받은 DB의 수정된 날짜를 SP에 저장하도록한다.
(Handler에서 Message 처리)
 // DB를 다 받고 시스템에 저장했으므로 SharedPreferences에 최신 날짜로 저장한다.

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(Activity.this);
SharedPreferences.Editor editor = pref.edit();
editor.putLong("savedDBTime", lastModified);
editor.commit();

그리고 앱의 다음 과정으로 진행하면 된다.

여기까지가 서버에 있는 DB를 단말에 내려받아서 설치하는 과정이다.

다음으로 내려받은 DB를 어떻게 활용할 수 있는지 간단하게나마 소개하겠다.
그리고 내려받는 도중 3G, WiFi간 전환시 어떻게 대처할 수 있는지 개인적은 의견을 포스팅하겠다.





내용이 이상하거나, 잘못됐거나, 이해가 안되는 부분은 꼭 댓글로 남겨주시라~! 본인이 답변도 드리고, 추후에 방문한 분들께 더 나은 포스트를 제공해드리는데 큰 도움이 된다.