Seize the day

SEARCH RESAULT : 글 검색 결과 - 전체 글 (총 474개)

POST : Android Dev Study

content://im/chat 분석

갤럭시S9플러스 OS10에서 테스트했고, 통신3사의 새로운 RCS기반 메시징 데이타임.  colum index, column name, value 순으로 출력했다.

READ_SMS 권한이 필요

contentResolver.query(
            Uri.parse("content://im/chat"),
            null,
            null,
            null,
            "date DESC")?.use { cursor ->
            val columnCount = cursor.columnCount

            if (cursor.moveToFirst()) {
                for(i in 0 until columnCount) {
                    Log.e("__T", "[$i=${cursor.getColumnName(i)}] ${cursor.getString(i)}")
                }
            }
        }

2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [0=_id] 80
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [1=thread_id] 7
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [2=transaction_id] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [3=address] 15884000
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [4=date_sent] 1633164482772
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [5=date] 1633164487845
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [6=read] 1
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [7=status] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [8=type] 1
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [9=body] {"messageHeader":"[Web발신]","copyAllowed":true,"zoomAllowed":true,"card":"open_rich_card","layout":{"background":"#ffffff","paddingLeft":"16dp","paddingRight":"16dp","widget":"LinearLayout","width":"284dp","height":"content","orientation":"vertical","children":[{"paddingTop":"16dp","marginBottom":"8dp","widget":"ImageView","width":"content","height":"content","mediaUrl":"https://content.maapconnect.com/files/proxy/bot/KT0000000006462?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlVHlwZSI6ImltYWdlL3BuZyIsIm9yaWdpbmFsVXJsIjoiaHR0cHM6Ly9tZWRpYS5oZXJtZXMua3QuY29tL2RhdGEvTUVESUEvbWVzc2FnZWJhc2UvMzA5MTc3MzQ0NmMzNDg1ZDk2YTM4Nzk1ZWM1MGIxYjkuTFQtMjAwODIxMTA0MzI3MjgxLUtRYnoucG5nIn0.FVrj1clsmNCnTvme0yrlg_YNeATkGKXn3n5QTP5Yy8f0LWPvc_0yTL2mqmS98lfFU088tWnQe9vIHZZjAsuf2zCAJrb45HyO-GUitymY-NU_NKYdcwPTQOaOc02vXOLkJO1HG2jtuclpbao9PDpMe8okc5M2LcE5UFgJGDEwdb157gYe-5xsVJMDlwVfPT9sGYtswdfQDoeCKiY-_eiQkfSTIMo9iamwUHwFUXzfBwBUd4d3J5vf9SdS8p61qI2pEWBBetrQ3lsqzfOczIUzrqe2SrECfdsj6MBHQE6pDzPLb8n24k8YKOrxxO4D1_NCWPgax_0SmTeY2LMYfsH4pg","mediaContentType":"image/png","mediaFileSize":4369},{"paddingBottom":"8dp","widget":"LinearLayout","width":"match","height":"content","orientation":"vertical","children":[{"paddingBottom":"8dp","visibility":"visible","widget":"LinearLayout","width":"match","height":"content","orientation":"horizontal","children":[{"weight":1,"widget":"TextView","width":"match","height":"content","text":"KB국민BC(8026) ","textAlignment":"textStart","textStyle":"bold","textColor":"#4483cf","textSize":"14dp"},{"weight":1,"widget":"TextView","width":"match","height":"content","text":"김*정 님","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"}]},{"background":"#c0c0c0","marginBottom":"8dp","visibility":"visible","widget":"View","width":"match","height":"1dp"},{"paddingBottom":"8dp","visibility":"visible","widget":"LinearLayout","width":"match","height":"content","orientation":"horizontal","children":[{"weight":1,"widget":"TextView","width":"match","height":"content","text":"4,990원 승인","textAlignment":"textStart","textStyle":"bold","textColor":"#b91724","textSize":"18dp"},{"weight":1,"widget":"TextView","width":"match","height":"content","text":" 일시불 ","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"}]},{"paddingBottom":"8dp","visibility":"visible","widget":"LinearLayout","width":"match","height":"content","orientation":"horizontal","children":[{"weight":1,"widget":"TextView","width":"match","height":"content","text":"10/02 17:48","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"}]},{"paddingBottom":"8dp","visibility":"visible","widget":"LinearLayout","width":"match","height":"content","orientation":"horizontal","children":[{"weight":1,"widget":"TextView","width":"match","height":"content","text":"총누적","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"},{"weight":1,"widget":"TextView","width":"match","height":"content","text":"329,412원","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"}]},{"background":"#c0c0c0","marginBottom":"8dp","visibility":"visible","widget":"View","width":"match","height":"1dp"},{"paddingBottom":"8dp","visibility":"visible","widget":"LinearLayout","width":"match","height":"content","orientation":"horizontal","children":[{"weight":1,"widget":"TextView","width":"match","height":"content","text":"홈플러스 익스프레스","textAlignment":"textStart","textColor":"#404040","textSize":"14dp"}]}]}]},"suggestions":[{"action":{"displayText":"카드이용내역 확인(페이북앱)","urlAction":{"openUrl":{"url":"https://paybooc.co.kr/mobile/front/mapp/html/paybooc_app_download2.html"}},"postback":{"data":"set_by_chatbot_open_url"}}}]}
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [10=display_notification_status] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [11=seen] 1
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [12=message_type] 30
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [13=session_id] 90a654f25ea30d81d8bc493da0e9da77d7a4266d
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [14=content_type] application/vnd.gsma.openrichcard.v1.0+json
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [15=hidden] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [16=locked] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [17=displayed_counter] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [18=reserved] 0
2021-10-04 02:18:28.161 11666-11666/com.example.mms_test E/__T: [19=imdn_message_id] B01A0320211002174801811PmSiwp5AGBE6H
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [20=rcsdb_id] 81
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [21=user_alias] 
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [22=delivered_timestamp] 1633164487914
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [23=remote_uri] tel:+8215884000
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [24=service_type] 1
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [25=sim_slot] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [26=sim_imsi] 138724
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [27=recipients] 15884000
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [28=sticker_id] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [29=delivered_count] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [30=timedmsg_expiry] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [31=secret_mode] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [32=ext_info] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [33=app_id] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [34=msg_id] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [35=secret_message] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [36=mcloud_filename] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [37=favorite] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [38=using_mode] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [39=updated_timestamp] 1633164488739
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [40=from_address] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [41=device_name] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [42=safe_message] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [43=safe_image_path] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [44=spam_report] 0
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [45=creator] com.samsung.android.messaging
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [46=correlation_tag] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [47=object_id] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [48=cmc_prop] null
2021-10-04 02:18:28.162 11666-11666/com.example.mms_test E/__T: [49=is_bot] 1

 

body는 json으로 파싱하여 text를 얻을 수 있을 듯 하다. 안드로이드의 Native View의 속성을 그대로 json으로 구성했네.

{
  "messageHeader": "[Web발신]",
  "copyAllowed": true,
  "zoomAllowed": true,
  "card": "open_rich_card",
  "layout": {
    "background": "#ffffff",
    "paddingLeft": "16dp",
    "paddingRight": "16dp",
    "widget": "LinearLayout",
    "width": "284dp",
    "height": "content",
    "orientation": "vertical",
    "children": [
      {
        "paddingTop": "16dp",
        "marginBottom": "8dp",
        "widget": "ImageView",
        "width": "content",
        "height": "content",
        "mediaUrl": "https://content.maapconnect.com/files/proxy/bot/KT0000000006462?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmaWxlVHlwZSI6ImltYWdlL3BuZyIsIm9yaWdpbmFsVXJsIjoiaHR0cHM6Ly9tZWRpYS5oZXJtZXMua3QuY29tL2RhdGEvTUVESUEvbWVzc2FnZWJhc2UvMzA5MTc3MzQ0NmMzNDg1ZDk2YTM4Nzk1ZWM1MGIxYjkuTFQtMjAwODIxMTA0MzI3MjgxLUtRYnoucG5nIn0.FVrj1clsmNCnTvme0yrlg_YNeATkGKXn3n5QTP5Yy8f0LWPvc_0yTL2mqmS98lfFU088tWnQe9vIHZZjAsuf2zCAJrb45HyO-GUitymY-NU_NKYdcwPTQOaOc02vXOLkJO1HG2jtuclpbao9PDpMe8okc5M2LcE5UFgJGDEwdb157gYe-5xsVJMDlwVfPT9sGYtswdfQDoeCKiY-_eiQkfSTIMo9iamwUHwFUXzfBwBUd4d3J5vf9SdS8p61qI2pEWBBetrQ3lsqzfOczIUzrqe2SrECfdsj6MBHQE6pDzPLb8n24k8YKOrxxO4D1_NCWPgax_0SmTeY2LMYfsH4pg",
        "mediaContentType": "image/png",
        "mediaFileSize": 4369
      },
      {
        "paddingBottom": "8dp",
        "widget": "LinearLayout",
        "width": "match",
        "height": "content",
        "orientation": "vertical",
        "children": [
          {
            "paddingBottom": "8dp",
            "visibility": "visible",
            "widget": "LinearLayout",
            "width": "match",
            "height": "content",
            "orientation": "horizontal",
            "children": [
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "KB국민BC(8026) ",
                "textAlignment": "textStart",
                "textStyle": "bold",
                "textColor": "#4483cf",
                "textSize": "14dp"
              },
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "김*정 님",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              }
            ]
          },
          {
            "background": "#c0c0c0",
            "marginBottom": "8dp",
            "visibility": "visible",
            "widget": "View",
            "width": "match",
            "height": "1dp"
          },
          {
            "paddingBottom": "8dp",
            "visibility": "visible",
            "widget": "LinearLayout",
            "width": "match",
            "height": "content",
            "orientation": "horizontal",
            "children": [
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "4,990원 승인",
                "textAlignment": "textStart",
                "textStyle": "bold",
                "textColor": "#b91724",
                "textSize": "18dp"
              },
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": " 일시불 ",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              }
            ]
          },
          {
            "paddingBottom": "8dp",
            "visibility": "visible",
            "widget": "LinearLayout",
            "width": "match",
            "height": "content",
            "orientation": "horizontal",
            "children": [
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "10/02 17:48",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              }
            ]
          },
          {
            "paddingBottom": "8dp",
            "visibility": "visible",
            "widget": "LinearLayout",
            "width": "match",
            "height": "content",
            "orientation": "horizontal",
            "children": [
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "총누적",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              },
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "329,412원",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              }
            ]
          },
          {
            "background": "#c0c0c0",
            "marginBottom": "8dp",
            "visibility": "visible",
            "widget": "View",
            "width": "match",
            "height": "1dp"
          },
          {
            "paddingBottom": "8dp",
            "visibility": "visible",
            "widget": "LinearLayout",
            "width": "match",
            "height": "content",
            "orientation": "horizontal",
            "children": [
              {
                "weight": 1,
                "widget": "TextView",
                "width": "match",
                "height": "content",
                "text": "홈플러스 익스프레스",
                "textAlignment": "textStart",
                "textColor": "#404040",
                "textSize": "14dp"
              }
            ]
          }
        ]
      }
    ]
  },
  "suggestions": [
    {
      "action": {
        "displayText": "카드이용내역 확인(페이북앱)",
        "urlAction": {
          "openUrl": {
            "url": "https://paybooc.co.kr/mobile/front/mapp/html/paybooc_app_download2.html"
          }
        },
        "postback": {
          "data": "set_by_chatbot_open_url"
        }
      }
    }
  ]
}

 

Json to text

    fun jsonTest() {
        val jsonObject = JSONObject(json)
        val layout = jsonObject.getJSONObject("layout")
        val text = when (layout.getString("widget")) {
            "LinearLayout" -> parseLinearLayout(layout)
            "TextView" -> layout.getString("text")
            else -> ""
        }
        Log.e("__T",  text)
    }

    private fun parseLinearLayout(widget: JSONObject): String {
        val sb = StringBuilder()
        val isVertical = widget.getString("orientation") == "vertical"
        val children = widget.getJSONArray("children")
        for(i in 0 until children.length()) {
            val widget = children.getJSONObject(i)
            val widgetName = widget.getString("widget")
            if (widgetName == "LinearLayout") {
                val text = parseLinearLayout(widget)
                sb.append(text)
                if (isVertical) sb.append("\n")
            } else if (widgetName == "TextView") {
                val text = widget.getString("text")
                sb.append(text)
                if (isVertical) sb.append("\n")
            }
        }
        return sb.toString()
    }

결과는 

KB국민BC(8026) 김*정 님
4,990원 승인 일시불 
10/02 17:48
총누적329,412원
홈플러스 익스프레스
top

posted at

2021. 10. 4. 02:30


POST : Android Dev Study

WorkManager 실습

매일마다 새벽 0시 부터 6시 사이의 랜덤한 시간에 sync()를 호출하는 기능을 WorkManager를 이용해서 구현하기. SyncWorker에는 파라미터가 없어서 최대한 간단히 

WorkManager를 사용한 백그라운드 작업 - Kotlin  |  Android 개발자  |  Android Developers 
WorkManager periodicity. A deep dive into how WorkManager handle… | by Pietro Maggi | Android Developers | Medium 참고

 

build.gradle

implementation "androidx.work:work-runtime-ktx:2.3.4"

Settings.kt

val isAutoSync = SettingBoolean("isAutoSync", false)
val nextSyncTime = SettingLong("nextSyncTime", 0L)
val lastSyncTime = SettingLong("lastSyncTime", 0L)

SyncWorker.kt

class SyncWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {

    override fun doWork(): Result {
        if (Settings.isAutoSync.value) {
            try {
                SyncManager().sync(true)

                Settings.lastSyncTime.value = System.currentTimeMillis()
            } catch (_: Exception) {
                Settings.lastSyncTime.value = -1L
                DUtils.notReached()
            }

            SyncWorkerManager.enqueueWorker(context)
        } else {
            Settings.nextSyncTime.value = 0L
        }
        return Result.success()
    }
}


object SyncWorkerManager {

    private const val UNIQUE_WORK_NAME = "SYNC_WORKER"

    fun enqueueWorker(context: Context) {
        val currentDate = Calendar.getInstance()
        val dueDate = Calendar.getInstance()
        val random = SecureRandom()
        dueDate.add(Calendar.DATE, 1) // tomorrow
        dueDate.set(Calendar.HOUR_OF_DAY, random.nextInt(7)) // 0 ~ 6
        dueDate.set(Calendar.MINUTE, random.nextInt(60)) // 0 ~ 59
        dueDate.set(Calendar.SECOND, random.nextInt(60)) // 0 ~ 59
        if (dueDate.before(currentDate)) {
            dueDate.add(Calendar.HOUR_OF_DAY, 24)
        }
        val timeDiff = dueDate.timeInMillis - currentDate.timeInMillis

        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()

        val dailyWorkRequest = OneTimeWorkRequestBuilder<SyncWorker>()
            .setConstraints(constraints)
            .setInitialDelay(timeDiff, TimeUnit.MILLISECONDS)
            .build()

        WorkManager
            .getInstance(context)
            .beginUniqueWork(
                UNIQUE_WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                dailyWorkRequest
            )
            .enqueue()

        Settings.nextSyncTime.value = dueDate.timeInMillis
    }

    fun cancelWork(context: Context) {
        WorkManager.getInstance(context).cancelUniqueWork(UNIQUE_WORK_NAME)
        Settings.nextSyncTime.value = 0L
    }
}

SettingsMainActivity.kt

private void toggleAutoSyncView() {
        boolean isChecked = !autoSyncView.isChecked();
        Settings.INSTANCE.isAutoSync().setValue(isChecked);
        if (isChecked) {
            SyncWorkerManager.INSTANCE.enqueueWorker(this);
        } else {
            SyncWorkerManager.INSTANCE.cancelWork(this);
        }
        updateAutoSyncView(isChecked);
    }

    private void updateAutoSyncView(boolean isAutoSync) {
        autoSyncView.setChecked(isAutoSync);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM/dd HH:mm");
        StringBuilder sb = new StringBuilder();
        sb.append("last: ");
        long lastSyncTime = Settings.INSTANCE.getLastSyncTime().getValue();
        if (lastSyncTime == 0L) {
            sb.append("None");
        } else if (lastSyncTime < 0L) {
            sb.append("Error");
        } else {
            sb.append(simpleDateFormat.format(new Date(lastSyncTime)));
        }
        if (isAutoSync) {
            sb.append(", next: ");
            long nextSyncTime = Settings.INSTANCE.getNextSyncTime().getValue();
            if (nextSyncTime <= 0L) {
                sb.append("None");
            } else {
                sb.append(simpleDateFormat.format(new Date(nextSyncTime)));
            }
        }
        autoSyncView.getDescriptionView().setVisibility(View.VISIBLE);
        autoSyncView.getDescriptionView().setText(sb.toString());
    }

 

대충 돌아는 가는 듯..

top

posted at

2021. 10. 3. 00:14


POST : Backend study

Nginx에 gzip 적용하기

http://nginx.org/en/docs/http/ngx_http_gzip_module.html

 

Module ngx_http_gzip_module

Module ngx_http_gzip_module The ngx_http_gzip_module module is a filter that compresses responses using the “gzip” method. This often helps to reduce the size of transmitted data by half or even more. When using the SSL/TLS protocol, compressed respons

nginx.org

gzip            on;
gzip_min_length 1000;
gzip_types      text/plain application/xml application/json;

간단히 세줄 추가로 deno+oak 서버에서 application/json 형식의 응답을 gzip으로 압축하여 클라이언트로 내려주었다.

 

Android에서 gzip 핸들링

요청 헤더에 Accetp-Encodig: gzip 추가 (by OkHttp3)

// "Accept-Encoding: gzip" 으로 gzip을 처리할 수 있음을 서버에 전달
val body = RequestBody.create(JSON_CONTENT_TYPE, jsonObject.toString(INDENT))
val request = Request.Builder()
                .url(getUrl(path))
                .header("Content-Type", "application/json; charset=utf-8")
                .header("Accept-Encoding", "gzip")
                .post(body)
                .build()
val response = httpClient.newCall(request).execute()

// "Content-Encoding: gzip" 응답에 gzip으로 압축된 컨텐츠인지 체크하여 unzip 처리
var bodyString: String?
            // gzip 디코딩
            if (response.header("Content-Encoding") == "gzip" ||
                response.header("content-encoding") == "gzip"
            ) {
                val responseBody = response.body() ?: throw IOException("body is null")
                BufferedInputStream(GZIPInputStream(responseBody.byteStream())).use { input ->
                    ByteArrayOutputStream().use { baos ->
                        val ba = ByteArray(1024)
                        while (true) {
                            val len = input.read(ba)
                            if (len == -1) break
                            baos.write(ba, 0, len)
                        }

                        bodyString = String(baos.toByteArray())
                    }
                }
            } else {
                bodyString = response.body()?.string()
            }

 

Android에서 요청시 압축하여 보내기

// data 압축
val data: ByteArray = jsonObject.toString().toByteArray()
val arr = ByteArrayOutputStream()
val zipper: OutputStream = GZIPOutputStream(arr)
zipper.write(data)
zipper.close()

val body = RequestBody.create(JSON_CONTENT_TYPE, arr.toByteArray())

// "Content-Encoding: gzip" 추가
val request = Request.Builder()
    .url(getUrl(path))
    .header("Content-Type", "application/json; charset=utf-8")
    .header("Content-Encoding", "gzip")
    .header("Accept-Encoding", "gzip")
    .post(body)
    .build()
val response = httpClient.newCall(request).execute()

이 방법으로 gzip 인코딩하여 요청할 수 있으나, 안타깝게도 deno + oak 서버에서 gzip 인코딩 요청을 지원하지 않는다.

 

Cpp에서 gzip 핸들링

https://zlib.net/ 에서 최신 소스를 받는다. Visual studio 2019 프로젝트에 추가하고, Precompiled header 사용하지 않도록 설정

https://github.com/mapbox/gzip-hpp  zlip를 쉽게 사용할 수 있도록 hpp로만 구현된 wrapper 코드 다운로드, hpp이므로 프로젝트의 구성파일에 추가할 필요는 없음

gzip 인지 판단하기

https://docs.microsoft.com/ko-kr/windows/win32/wininet/retrieving-http-headers 에서 참고 HttpClient로 WinInet을 사용하기 때문에..

bool HasGzipHeader(HINTERNET hHttp, LPCSTR  headerKey)
{
	DWORD dwSize = 20;
	LPVOID lpOutBuffer = new char[dwSize];

	StringCchPrintfA((LPSTR)lpOutBuffer, dwSize, headerKey);

retry:

	if (!HttpQueryInfoA(hHttp, HTTP_QUERY_CUSTOM,
		(LPVOID)lpOutBuffer, &dwSize, NULL))
	{
		if (GetLastError() == ERROR_HTTP_HEADER_NOT_FOUND)
		{
			// Code to handle the case where the header isn't available.
			delete[] lpOutBuffer;
			return false;
		}
		else
		{
			// Check for an insufficient buffer.
			if (GetLastError() == ERROR_INSUFFICIENT_BUFFER)
			{
				// Allocate the necessary buffer.
				delete[] lpOutBuffer;
				dwSize = dwSize * 2;
				lpOutBuffer = new char[dwSize];

				// Rewrite the header name in the buffer.
				StringCchPrintfA((LPSTR)lpOutBuffer, dwSize, headerKey);

				// Retry the call.
				goto retry;
			}
			else
			{
				// Error handling code.
				delete[] lpOutBuffer;
				return false;
			}
		}
	}

	bool ret = (memcmp(lpOutBuffer, "gzip\0", 5) == 0);
	delete[] lpOutBuffer;
	return ret;
}


bool isGzipContents = HasGzipHeader(hRequest, "Content-Encoding") || HasGzipHeader(hRequest, "content-encoding");

msdn 샘플소스에서 실수가 있는 듯..
프로젝트가 Unicode 프로젝트이므로 HttpQueryInfoA 를 사용해야 한다.
ERROR_INSUFFICIENT_BUFFER 일때 dwSize = dwSize * 2 를 빠트린듯하다. 

gzip 압축해제

샘플 코드에서 두 줄만 가져와서 간단히 구현됨

std::string resultBody;
BOOL ret = _CallHttpRequest(resultCode, url.data(), resultBody, postData.data(), &requestParams);
if (resultCode.hasGzipHeader) {
    const char* compressed_pointer = resultBody.data();
    std::string result = gzip::decompress(compressed_pointer, resultBody.size());
}
top

posted at

2021. 9. 25. 13:50


POST : Flutter study

Flutter desktop (Windows 10) 테스트

Flutter가 메이저 업데이트되서 다시 공부를 시작한다. (공부 안 한지 2년쯤 됬음)

  • Windows에 Flutter 설치 Flutter | Windows install  참고
    • flutter-sdk/bin 폴더를 System path에 추가
  • Visual studio 2019  커뮤니티 버전은  설치되어 있음.
  • Desktop 프로젝트 생성후 프로그램 실행까지 명령어 몇 개로 테스트 되었음. 
  • Visual studio code에서 RUN 한 후에 코드 수정후 저장하니 바로 앱에 반영이 되었음. 
  • 소감
    • 아직 안정버전이 아님에도 오류없이 동작해서 좀 놀람. 보통 개발 분야에서 윈도즈는 좀 우선순위가 낮아서 문제가 생기는 경우가 많은데. 
    • Windows Flutter 플러그인을 만드는 법을 익혀야 제대로 개발이 될 것 같음. 
    • 2년전에는 일렉트론을 고민했으나, 자바스크립트 + 크롬엔진보다는 Dart가 확실히 나아보임.
    • Flutter 중급까지 익혀보자 
top

posted at

2021. 9. 16. 02:26


POST : Android Dev Study

Android AES encryption + Cpp AES decryption

Android에서 AES 암호화하기

    private fun encrypt(jwt: String, name: String): String {
        val data = JSONObject()
            .put("jwt", jwt)
            .put("name", name)
            .toString()

        try {
            val key = EncryptUtils.generateSHA256("super-secret-key")
            val iv = ByteArray(16) { 0 }
            val skeySpec = SecretKeySpec(key, "AES")
            val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, IvParameterSpec(iv))

            val clearText = data.toByteArray()
            val data = ByteArray(((clearText.size / 16) + 1) * 16)
            System.arraycopy(clearText, 0, data, 0, clearText.size)
            val encrypted = cipher.doFinal(data)
            return Base64.encodeToString(encrypted, Base64.NO_WRAP)
        } catch (ex: Exception) {
            DUtils.notReached(ex)
            throw ex;
        }
    }
    
    @WorkerThread
    @Throws(Exception::class)
    fun qrcodeAccept(code: String) {
        val data = encrypt(getJwtForDesktop(), getDbName())
        val parameter = JSONObject()
                .put("encrypted", true)
                .put("data", data)

        request("/v1/qrcode-accept/$code", parameter, true)
    }

 

Cpp 클라이언트에서 복호화하기

간단히 AES를 풀수있는 라이브러리 SergeyBel/AES: C++ AES implementation (github.com) 를 사용.
간단히 json을 다룰수 있는 라이브러리 nlohmann/json: JSON for Modern C++ (github.com) 사용

int CDlgQrcode::handleTokenData(std::string& data) {

	dfx::ByteArray encrypted;
	if (!dfx::Base64_DecodeA(data.c_str(), encrypted))
		return 1;

	const char* key = "super-secret-key";
	BYTE aesKey[32];
	dsMakeSHA256((BYTE*)key, strlen(key), aesKey);

	BYTE ivKey[16] = { 0 };
	ZeroMemory(ivKey, 16);

	AES aes(256);
	unsigned char* result = aes.DecryptCBC(encrypted.GetBuffer(), encrypted.GetSize(), aesKey, ivKey);

	std::string jsonString = (char*)result;
	delete[] result;
    
        try {
		json jsonObject = json::parse(jsonString.c_str());
		std::string jwt = jsonObject.at("jwt").get<std::string>();
		std::string name = jsonObject.at("name").get<std::string>();
                CString dbName = dfx::Utf8toUnicode(name.data());
		CDialog::OnOK();
	}
	catch (const std::exception& e) {
		::AfxMessageBox(L"json parsing failed:\n" + dfx::Utf8toUnicode(jsonString.c_str()));
	}    
        return 0;
}

 

 

보안 강화를 위해서 JWT 토큰 만들기

h5p9sl/hmac_sha256: Minimal HMAC-SHA256 implementation in C / C++ (github.com) 를 코드를 이용한다. 간단히 소스파일 2개만 추가해서 구현..

std::string base64url_encode(BYTE* data, int size) {
	CAtlStringA base64 = dfx::Base64_EncodeA(data, size);
	base64.Replace("=", "");
	base64.Replace("+", "-");
	base64.Replace("/", "_");
	return base64;
}

std::string getJwtAuth(std::string code) {

	const std::string header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";

	CTime ct = CTime::GetCurrentTime();
	__time64_t currentTimeSeconds = ct.GetTime();
	__time64_t exp = currentTimeSeconds + 60; // 60 seconds 유효
	std::string payload = string_format("{\"exp\":%ld, \"code\":\"", exp);
	payload += code;
	payload += "\"}";
	

	const std::string header_encoded = base64url_encode((BYTE*)header.data(), header.size());
	const std::string payload_encoded = base64url_encode((BYTE*)payload.data(), payload.size());
	const std::string data = header_encoded + "." + payload_encoded;

	const std::string key = "super-secret-key";

	BYTE hmac[32];
	hmac_sha256(key.data(), key.size(), data.data(), data.size(), hmac, 32);


	const std::string signiture = base64url_encode(hmac, 32);
	return header_encoded + "." + payload_encoded + "." + signiture;
}


deno/ oak에서 jwt-auth 헤더 확인

exp로 유효시간 확인, jwt의 payload안의 code와 일치하는지 확인

/**
 * verifyJwtAuth
 */
 import { hmac as createHmac} from "https://deno.land/x/crypto@v0.10.0/hmac.ts"
 import {encode as base64urlEncode, decode as base64urlDecode} from "https://deno.land/std@0.103.0/encoding/base64url.ts"
 
const PRIVATE_SECRET = new TextEncoder().encode("super-secret-key")

export function verifyJwtAuth(token: string, code: string) {
    if (token == null) {
        throwJwtError("empty token")
    }
    const [header, payload, signiture] = token.split('.')
    const value = new TextEncoder().encode(header + "." + payload)
    const hmacValue = createHmac('sha256', PRIVATE_SECRET, value)
    const signiture2 = base64urlEncode(hmacValue)
    if (signiture !== signiture2) {
        throwJwtError("invalid signiture")
    }
    const payloadJson: string = new TextDecoder().decode(base64urlDecode(payload))
    const payloadObj = JSON.parse(payloadJson)
    
    if (code !== payloadObj.code) {
        throwJwtError("invalid code")
    }

    const exp = payloadObj.exp || null
    if (typeof exp != "number") {
        throwJwtError("invalid exp")
    }
                
    if (exp <= Math.floor(Date.now()/1000)) {
        throw new ErrorCode("jwt auth error: expired token", kErrorJwtExpire)
    }
}
top

posted at

2021. 8. 29. 00:59


POST : Android Dev Study

cpp QRCode Login + Android QrCode scanning

CPP에서 Qrcode 생성하기

nayuki/QR-Code-generator: High-quality QR Code generator library in Java, TypeScript/JavaScript, Python, C++, C, Rust. (github.com) 간단히 파일 두 개로 구현된 라이브러리 이용하기 

 

GitHub - nayuki/QR-Code-generator: High-quality QR Code generator library in Java, TypeScript/JavaScript, Python, C++, C, Rust.

High-quality QR Code generator library in Java, TypeScript/JavaScript, Python, C++, C, Rust. - GitHub - nayuki/QR-Code-generator: High-quality QR Code generator library in Java, TypeScript/JavaScri...

github.com

 

오랜만에 MFC 하려니 힘들구나.. 기억이 안나..

void CDlgQrcode::OnPaint() {
	CPaintDC dc(this);

	if (url_.IsEmpty()) {
		return;
	}

	// Manual operation
	CAtlStringA url = dfx::toUtf8(url_.GetBuffer(0));
	QrCode qr1 = QrCode::encodeText(url.GetBuffer(0), QrCode::Ecc::LOW);

	CBrush brushWhite, brushBlack;
	brushWhite.CreateSolidBrush(RGB(255, 255, 255));
	brushBlack.CreateSolidBrush(RGB(0, 0, 0));
	
	const int cellSize = 10;

	for (int y = 0; y < qr1.getSize(); y++) {
		for (int x = 0; x < qr1.getSize(); x++) {
			int start = (x+1) * cellSize; 
			int end = (y+1) * cellSize;
			CRect rc(start, end, start + cellSize, end + cellSize);
			if (qr1.getModule(x, y)) {
				dc.FillRect(&rc, &brushBlack);
			}
			else {
				dc.FillRect(&rc, &brushWhite);
			}
		}
	}
}

 

deno/ oak 서버에서  long polling하면서, Android에서 Qrcode 스캔하여 응답하기를 30초간 기다리기

router
    .get(v1 + "qrcode-login/:code", qrcodeLoginGetMethod)
    .post(v1 + "qrcode-login/:code", qrcodeLoginPostMethod)
    .post(v1 + "qrcode-accept/:code", qrcodeAccept)
    
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const requestMap = new Map<string, any>()

// Chrome으로 열 경우, 안내 페이지를 연다. 안드로이드 앱의 SchemeActivity를 여는 lt:// link를 제공한다.
export const qrcodeLoginGetMethod = (context: any) => {
    const response = context.response
    const code: string = context.params.code
    if (!isValidQrcode(code)) {
        response.body = "<html><body><h1>Invalid qrcode login</h1></body></html>"
    } else {
        response.body = `<html>
    <body>
        <h1>Qrcode login</h1>
        <br> 
        <button onClick="location.href='lt://qrcode-login/${code}';"><h3>Open app(If you have app)</h3></button></p>
        <button onClick="location.href='https://play.google.com/store/apps/details?id=com.mdiwebma.screenshot';"><h3>Install app</h3></button>
    </body>
</html>`
    }
    response.status = 200    
}


export const qrcodeLoginPostMethod = async (context: any) => {
    const request = context.request
    const ip = request.ip
    console.log('qrcodeLogin ip='+ip)

    const response = context.response        
    const code: string = context.params.code
    
    if (!isValidQrcode(code)) throw new ErrorCode("Invalid Qrcode login", kErrorQrcode)
    
    await qrcodeLoginByLongPolling(code, ip, response)
}

async function qrcodeLoginByLongPolling(code: string, ip: string, response: any) {
    try {
        requestMap.set(code, {ip: ip})

        for(let i = 0; i< 10; i++) {
            await sleep(3000)
            //console.log(`${guid} ${i}`)
            
            const value = requestMap.get(code)
            const encrypted = value.encrypted
            if (typeof encrypted == "boolean") {
                handleResult(response, value)        
                return
            }
        }

        throw new ErrorCode("Qrcode login timeout", kErrorQrcode)
    } catch(err) {
        console.log(err)
        throw new ErrorCode(err.toString(), kErrorQrcode)
    } finally {
        requestMap.delete(code)
    }
}

isValidQrcode() : qrcode는 hash하여, 간단히 자체적으로 validation을 체크한다. 

qrcodeLoginGetMethod() : 안드로이드앱이 아닌, 일반 브라우저에서 Scanning한 경우라면 qrcodeLoginGetMethod 가 호출된다. 적당히 안내를 해서 앱으로 연결시키던지, 앱 다운로드 페이지로 안내한다. 

qrcodeLoginPostMethod() : Cpp 클라이언트에서 연결을 하고 있다가,  안드로이드앱에서 qrcode-accept/{code} 를 호출하여, requestMap에 값을 입력하면 그 값을 cpp 클라이언트에 응답한다. 

 

안드로이드 Qrcode 스캐닝

https://github.com/dm77/barcodescanner 의 라이브러리를 이용한다.

build.gradle

implementation 'me.dm7.barcodescanner:zxing:1.9.13'

 

AndroidManifest.xml

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

<activity
            android:name=".activity.ScannerActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:screenOrientation="portrait" />
            
<activity
            android:name=".activity.SchemeActivity"
            android:launchMode="singleTask"
            android:theme="@style/AppTheme.Transparent">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="lt" />
            </intent-filter>
            <!--            App link guide -->
            <!--            https://developer.android.com/studio/write/app-link-indexing?hl=ko-->
            <!--            https://developer.android.com/guide/topics/manifest/data-element.html-->
            <!--            <intent-filter>-->
            <!--                <action android:name="android.intent.action.VIEW" />-->

            <!--                <category android:name="android.intent.category.DEFAULT" />-->
            <!--                <category android:name="android.intent.category.BROWSABLE" />-->
            <!--                -->
            <!--                <data-->
            <!--                    android:scheme="https"-->
            <!--                    android:host="api.blabla.com"-->
            <!--                    android:port="443"-->
            <!--                    android:pathPrefix="/lt/v1/qrcode-login/" />-->
            <!--            </intent-filter>-->
</activity>

App link는 OS가 자동적으로 특정 url을 app으로 리다이렉트 시켜주는 것 같다.  다만 서버에 디지털 에셋 링크 파일을 만들어서 업로드해 두어야한다.  https://developer.android.com/studio/write/app-link-indexing?hl=ko   참고

 

ScannerActivity

import android.Manifest
import android.os.Bundle
import com.google.zxing.Result
import com.mdiwebma.base.BaseAppCompatActivity
import com.mdiwebma.base.utils.ToastUtils
import me.dm7.barcodescanner.zxing.ZXingScannerView

class ScannerActivity : BaseAppCompatActivity(0), ZXingScannerView.ResultHandler {

    private var scannerView: ZXingScannerView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        scannerView = ZXingScannerView(this)
        setContentView(scannerView)
        
        checkPermissions(arrayOf(Manifest.permission.CAMERA)) { isAllGranted, _ ->
            if (isAllGranted) {
                scannerView?.setResultHandler(this)
                scannerView?.startCamera()
            } else {
                finish()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        // TODO resumeCamera 등 처리필요        
    }

    override fun onPause() {
        super.onPause()
        scannerView?.stopCamera()
    }

    override fun handleResult(rawResult: Result) {
        if ("/qrcode-login/" in rawResult.text) {
            val nodes = rawResult.text.split("/")
            val code = nodes.last()
            if (code.length != 36) {
                scannerView?.resumeCameraPreview(this)
                ToastUtils.show("hmm??..$code")
                return
            }

            lifecycleScope.launch {
                try {
                    withContext(Dispatchers.IO) {
                        SyncManager().qrcodeAccept(code)
                    }
                } catch (ex: Exception) {
                    ToastUtils.show(ex.toString())
                }
                finish()
            }
        } else {
            // retry
            ToastUtils.show(rawResult.text)
            scannerView?.resumeCameraPreview(this)
        }
    }
}

 

모바일 크롬에서 lt:// 링크를 클릭했을때 앱으로 열리게..
SchemeActivity.kt

class SchemeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val deepLinkUri = intent.data ?: run {
            finish()
            return
        }
        val url = deepLinkUri.toString()
        if ("/qrcode-login/" in url) {
            // TODO: code 추출
            // jwt 를 만들어서,
            // qrcode-accept/{code} 로 전달..
        }
        finish()
    }
}

 

top

posted at

2021. 8. 29. 00:31


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크