안드로이드 SMS와 MMS 읽어 저장하기 (2)

2018.03.29 추가사항

기존에 메세지를 읽어오는 getList 함수는 SMS와 MMS의 전체 크기를 읽는다. 때문에 SMS를 읽고 저장한 후 MMS를 읽어 저장하려고 할 경우에 문제가 있다. 메세지 리스트는 이미 SMS 리스트가 저장된 상태이기에 MMS의 저장 크기 또한 SMS 리스트의 크기로 인식되는 버그가 발생한다.

기존 코드는 메세지 리스트에 저장된 메세지의 크기를 바탕으로 SMS와 MMS를 저장할 것인지의 여부를 결정한다. 즉, 메세지는 읽되 리스트의 크기로 인해 해당 메세지들이 이미 저장되어 있다고 판단하여 문제가 발생한다. 따라서 SMS와 MMS의 리스트를 따로 불러오는 함수가 필요하다.

따라서 메세지 타입을 저장하는 변수를 MessageVO 클래스에, 메세지 타입에 따라 해당 타입의 메세지를 반환하는 fun getListBy(messageType: Boolean): RealmResults 함수를 MessageList 클래스에 추가했다. (두 종류의 메세지를 합칠 때 발생하는 문제이므로 SMS, 혹은 MMS만 읽어 저장할 경우에는 해당 함수를 추가할 필요가 없다.)



SMS의 데이터를 읽고 저장하는 과정은 다음의 이전 글을 참고하시기 바란다.

안드로이드 SMS와 MMS 읽어 저장하기 (1)

MMS를 읽는 과정은 꽤 까다로웠다. 발신인의 휴대폰 번호와 메세지 내용 등을 한 번에 읽을 수 있는 SMS와 달리, MMS는 메세지 텍스트가 있는 content Uri와 발신인의 번호가 있는 Uri가 다르기 때문이다. 게다가 MMS는 텍스트 외의 것 또한 포함될 수 있다는 특징을 가지고 있다. 하지만 본인은 이미지가 아닌 텍스트만 필요했고 이를 읽어 Realm에 저장하는 방법을 Kotlin 코드로 다루려고 한다.

MMS를 읽기 위해 본인이 참고한 링크는 다음과 같다. 이미지 처리의 경우는 다음 첫 번째 링크를 참조하시면 되겠다.

How to Read MMS Data in Android? 안드로이드 MMS 모니터링

메세지를 저장하기 위한 모델, Realm과 상호작용하는 클래스, SMS와 MMS에서 공통으로 사용되는 변수 및 함수를 모은 인터페이스를 만드는 과정은 안드로이드 SMS와 MMS 읽어 저장하기 (1)의 1~3 과정과 동일하므로 생략하겠다. 본인은 먼저 메소드의 기능을 설명한 후 마지막에 전체 코드를 덧붙여 이해를 도우려 한다.

1. 멤버변수

override var messageList: MessageList = MessageList(realm) // Realm과의 상호작용을 위한 객체
private val idList = ArrayList<String>()// MMS의 id를 저장하기 위한 ArrayList
private val uri = Uri.parse("content://mms/inbox") // 기본 Uri
private var contentResolver: ContentResolver by Delegates.notNull()
// 쿼리를 불러오기 위한 contentResolver

2. 메세지를 가져오는 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun gatherMessages(context: Context) {
        val projection = arrayOf("_id", "ct_t", "date")//각각 메세지 id, type(텍스트인지 이미지인지 등), date
        contentResolver = context.contentResolver
        val cursor =
                contentResolver.query(uri, projection, null, null, "date DESC")

        setIdList() // 3. MMS id를 리스트에 저장하는 메소드

        if (cursor.moveToFirst()) {
            for (i: Int in messageList.getListBy(MessageVO.TYPE_MMS).size until cursor.count) {
                // 4. 메세지 inbox로부터 데이터베이스에 저장할 데이터를 오브젝트 리스트에 추가하는 메소드
                setDbFieldsFromMessageInbox(cursor)
                cursor.moveToNext()
            }
            cursor.close()
        }
    }

3. MMS 메세지의 id를 가져와 리스트에 저장하는 메소드

앞서 말했다시피 MMS는 같은 Uri에 주소와 텍스트가 있는 것이 아니다. 따라서 먼저 메세지의 id를 가져와 해당 id를 사용해 전화번호와 텍스트를 각각 얻어야 한다. id를 쓰지 않은 초반엔 발신인 번호랑 텍스트의 매칭이 이상해져 삽질을 했었다..ㅜㅜ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private fun setIdList() {
        // projection은 array이어야 하므로 arrayOf를 사용해 id의 Key 값을 입력
        val projection: Array<String> = arrayOf("_id")
        val cursor =
                contentResolver.query(uri, projection, null, null, "date DESC")

        if (cursor.moveToFirst()) {
            do {
                val mmsId = cursor.getString(cursor.getColumnIndexOrThrow("_id"))
                idList.add(mmsId)
            } while (cursor.moveToNext())
        }

        cursor.close()
    }

4. 메세지 inbox로부터 데이터베이스에 저장할 데이터를 오브젝트 리스트에 추가하는 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
override fun setDbFieldsFromMessageInbox(cursor: Cursor) {
        // id 할당
        val mmsId = cursor.getString(cursor.getColumnIndexOrThrow("_id"))
        // 1000을 곱하지 않으면 모든 date의 year가 1970년으로 표기됨
        val date = cursor.getLong(cursor.getColumnIndexOrThrow("date")) * 1000
        val mmsDate = Date(java.lang.Long.valueOf(date)) // 날짜 할당
        val phoneNumber: String = getPhoneNumber(mmsId)// 5. 메세지의 전화번호를 가져오는 메소드
        // MMS의 id에 해당하는 내용을 불러오기 위해 selection에 조건을 할당
        val selection = "mid=$mmsId"
        val partUri = Uri.parse("content://mms/part")
        val partCursor =
                contentResolver.query(partUri, null, selection, null, null)

        if (partCursor.moveToFirst()) {
            do {
                // MMS의 타입을 가져옴
                val type = partCursor.getString(partCursor.getColumnIndexOrThrow("ct"))
                if (type == "text/plain") {
                    val body = getMmsBody(partCursor) // 6. 메세지의 몸체를 가져오는 메소드
                    // 이미 데이터베이스에 저장된 메세지인지 확인하고 아니면 저장
                    if (messageList.getBodyNumbersSameWith(body) == 0) {
                        messageList.addToList(phoneNumber, mmsDate, body, MessageVO.TYPE_MMS)
                    }
                }
            } while (partCursor.moveToNext())
        }
        partCursor.close()
    }

5. 메세지의 전화번호를 가져오는 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private fun getPhoneNumber(id: String): String {
        val selection = "msg_id=$id"
        val phoneNumberUri = Uri.parse("content://mms/$id/addr")
        val cursor =
                contentResolver.query(phoneNumberUri, null, selection, null, null)
        var phoneNumber: String? = null

        if (cursor.moveToFirst()) {
            do {
                val address = cursor.getString(cursor.getColumnIndexOrThrow("address"))
                if (address != null) {
                    phoneNumber = address.replace("-", "")
                }
            } while (cursor.moveToNext())
        }

        cursor.close()
        return phoneNumber!!
    }

6. 메세지의 몸체를 가져오는 메소드

private fun getMmsBody(partCursor: Cursor): String {
       val partId = partCursor.getString(partCursor.getColumnIndexOrThrow("_id"))
       val data = partCursor.getString(partCursor.getColumnIndexOrThrow("_data"))
       return if (data != null) {
           // 7. 메세지의 텍스트를 가져오는 메소드, 아래의 코드를 포괄함
           getMessageText(contentResolver, partId)
       } else {
           partCursor.getString(partCursor.getColumnIndexOrThrow("text"))
       }

   }

7. 메세지의 텍스트를 가져오는 메소드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun getMessageText(contentResolver: ContentResolver, id: String): String {
        val partUri = Uri.parse("content://mms/part/$id")
        val stringBuilder = StringBuilder()
        val inputStream = contentResolver.openInputStream(partUri)

        if (inputStream != null) {
            val inputStreamReader = InputStreamReader(inputStream, "UTF-8")
            val bufferedReader = BufferedReader(inputStreamReader)
            var temp = bufferedReader.readLine()
            while (temp != null) {
                stringBuilder.append(temp)
                temp = bufferedReader.readLine()
            }
            inputStream.close()
        }

        return stringBuilder.toString()
    }

코드 전문은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class MmsReader(realm: Realm) : MessageReader {
    override var messageList: MessageList = MessageList(realm)
    private val idList = ArrayList<String>()
    private val uri = Uri.parse("content://mms/inbox")
    private var contentResolver: ContentResolver by Delegates.notNull()

    private fun setIdList() {
        val projection: Array<String> = arrayOf("_id")
        val cursor =
                contentResolver.query(uri, projection, null, null, "date DESC")

        if (cursor.moveToFirst()) {
            do {
                val mmsId = cursor.getString(cursor.getColumnIndexOrThrow("_id"))
                idList.add(mmsId)
            } while (cursor.moveToNext())
        }

        cursor.close()
    }

    override fun gatherMessages(context: Context) {
        val projection = arrayOf("_id", "ct_t", "date")
        contentResolver = context.contentResolver
        val cursor =
                contentResolver.query(uri, projection, null, null, "date DESC")

        setIdList()

        if (cursor.moveToFirst()) {
            for (i: Int in messageList.getListBy(MessageVO.TYPE_MMS).size until cursor.count) {
                setDbFieldsFromMessageInbox(cursor)
                cursor.moveToNext()
            }
            cursor.close()
        }
    }

    override fun setDbFieldsFromMessageInbox(cursor: Cursor) {
        val mmsId = cursor.getString(cursor.getColumnIndexOrThrow("_id"))
        // If you do not multiply 1000 to date, then all year of mms printed 1970
        val date = cursor.getLong(cursor.getColumnIndexOrThrow("date")) * 1000
        val mmsDate = Date(java.lang.Long.valueOf(date))
        val phoneNumber: String = getPhoneNumber(mmsId)
        val selection = "mid=$mmsId"
        val partUri = Uri.parse("content://mms/part")
        val partCursor =
                contentResolver.query(partUri, null, selection, null, null)

        if (partCursor.moveToFirst()) {
            do {
                val type = partCursor.getString(partCursor.getColumnIndexOrThrow("ct"))
                if (type == "text/plain") {
                    val body = getMmsBody(partCursor)
                    if (messageList.getBodyNumbersSameWith(body) == 0) {
                        messageList.addToList(phoneNumber, mmsDate, body, MessageVO.TYPE_MMS)
                    }
                }
            } while (partCursor.moveToNext())
        }
        partCursor.close()
    }

    private fun getMmsBody(partCursor: Cursor): String {
        val partId = partCursor.getString(partCursor.getColumnIndexOrThrow("_id"))
        val data = partCursor.getString(partCursor.getColumnIndexOrThrow("_data"))
        return if (data != null) {
            getMessageText(contentResolver, partId)
        } else {
            partCursor.getString(partCursor.getColumnIndexOrThrow("text"))
        }

    }

    @Throws(IOException::class)
    private fun getMessageText(contentResolver: ContentResolver, id: String): String {
        val partUri = Uri.parse("content://mms/part/$id")
        val stringBuilder = StringBuilder()
        val inputStream = contentResolver.openInputStream(partUri)

        if (inputStream != null) {
            val inputStreamReader = InputStreamReader(inputStream, "UTF-8")
            val bufferedReader = BufferedReader(inputStreamReader)
            var temp = bufferedReader.readLine()
            while (temp != null) {
                stringBuilder.append(temp)
                temp = bufferedReader.readLine()
            }
            inputStream.close()
        }

        return stringBuilder.toString()
    }

    @Throws(NumberFormatException::class)
    private fun getPhoneNumber(id: String): String {
        val selection = "msg_id=$id"
        val phoneNumberUri = Uri.parse("content://mms/$id/addr")
        val cursor =
                contentResolver.query(phoneNumberUri, null, selection, null, null)
        var phoneNumber: String? = null

        if (cursor.moveToFirst()) {
            do {
                val address = cursor.getString(cursor.getColumnIndexOrThrow("address"))
                if (address != null) {
                    phoneNumber = address.replace("-", "")
                }
            } while (cursor.moveToNext())
        }

        cursor.close()
        return phoneNumber!!
    }

이상이다. 부디 도움이 되었길 바란다.