2026/03/09

오늘의 이야기

지오펜싱 트리거 되는 위치의 명칭 표시 


지오펜스 앱을 수정하면서 또 하나를 찾았습니다.  구현해 보고 싶었던 것은 지오펜싱에서 찾은 위치에 대한 알림을 구현할 때 현재 내가 도착한 위치가 어떤 것 때문에 표시가 되고 있는지 알고 싶다는 것입니다.  물론 잘 아시는 분들은 이미 찾으셨을리라고 생각이 되지만, 이제 구현을 해 가고 있는 분들을 위해서 기억을 남겨 두고자 합니다. 


 



import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.billcoreatech.ontheway801.MainComposeActivity
import com.billcoreatech.ontheway801.R
import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofenceStatusCodes
import com.google.android.gms.location.GeofencingEvent

class GeofenceBroadcastReceiver : BroadcastReceiver() {

companion object {
var TAG = "GeofenceBroadcastReceiver"
internal const val ACTION_GEOFENCE_EVENT =
"action.ACTION_GEOFENCE_EVENT"
}


override fun onReceive(context: Context, intent: Intent) {

if (intent.action == ACTION_GEOFENCE_EVENT) {
Log.e(TAG, "onReceive ...")

val geofencingEvent = GeofencingEvent.fromIntent(intent)
// Test that the reported transition was of interest.
if (geofencingEvent != null) {

if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
Log.e("GeofenceBR", errorMessage)
return
}

// 트리거된 위치에 대한 정보를 취득해 보기 위해서...
var geofenceList = geofencingEvent.triggeringGeofences
var whereString = " "
if (geofenceList != null) {
for (geofence in geofenceList) {
Log.e(TAG, "requestId = ${geofence.requestId}")
whereString += "${geofence.requestId} "
}
}

when(val geofenceTransition = geofencingEvent.geofenceTransition) {
Geofence.GEOFENCE_TRANSITION_DWELL -> {
if (context != null) {
sendNotification(context, context.getString(R.string.DWell) + "[${whereString}]")
}
Log.e(TAG, context.getString(R.string.DWell))
}
Geofence.GEOFENCE_TRANSITION_ENTER -> {
if (context != null) {
sendNotification(context, context.getString(R.string.Enter) + "[${whereString}]")
}
Log.e(TAG, "Entered")
}
Geofence.GEOFENCE_TRANSITION_EXIT -> {
if (context != null) {
sendNotification(context, context.getString(R.string.Exit) + "[${whereString}]")
}
Log.e(TAG, "Exit")
}
else -> {
if (context != null) {
sendNotification(context, "Geofence ${geofenceTransition.hashCode()}")
}
Log.e(TAG, "geofenceTransition = ${geofenceTransition.hashCode()}")
}

}
} else {
Log.e(TAG, "geofencingEvent is null...")
}
}
}

@SuppressLint("MissingPermission")
private fun sendNotification(context : Context, messageBody: String) {
val intent = Intent(context, MainComposeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val extras = Bundle()
extras.putString("MSGRCV", messageBody)
intent.putExtras(extras)
val pendingIntent = PendingIntent.getActivity(
context, 0 /* Request code */, intent,
PendingIntent.FLAG_MUTABLE
)

val channelId: String = context.getString(R.string.default_notification_channel_id)
val channelName: CharSequence = context.getString(R.string.default_notification_channel_name)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val notificationChannel = NotificationChannel(channelId, channelName, importance)
notificationChannel.enableLights(true)
notificationChannel.lightColor = Color.RED
notificationChannel.enableVibration(true)
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)

val wearNotifyManager = NotificationManagerCompat.from(context)
val wearNotifyBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_locationnote_foreground)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(messageBody)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400))
.setDefaults(-1)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wearNotifyManager.createNotificationChannel(notificationChannel)
}
wearNotifyManager.notify(0, wearNotifyBuilder.build())
}
}

 


코드가 구현된 예제 화면


위에 기술된 전체 소스의 내용과 같이 트리거 되는 위치에 대한 정보를 취해서 그곳에 대한 알림을 전달하는 방식으로 내가 지정한 위치에 도달하였을 때 명칭을 표시하는 방법을 구현해 보게 됩니다. 


 


지오펜싱 위치 도달 알림.



 


트리거 명칭 전달 방법


참 저렇게 trigger 되게 하는 부분은 다음과 같이 적용했습니다.   아래처럼 geofenceList.add을 하게 되는 경우 builder을 설정하면서 setRequestId에 넣는 값을 표시하고자 하는 장소의 명칭을 넣었습니다.  그러면 위에 기술된 코드에서는 getRequestId로 값을 가져와 현재 표시되는 위치의 명칭을 보여주는 기능을 구현할 수 있습니다.


 


private fun doAddGeoFence(documents: ResponseBean.Documents) {
geofenceList.clear()
geofenceList.add(Geofence.Builder()
// Set the request ID of the geofence. This is a string to identify this
// geofence.
.setRequestId(documents.placeName)

// Set the circular region of this geofence.
.setCircularRegion(
documents.posY,
documents.posX,
sp.getFloat("aroundArea", 300f)
)

// Set the expiration duration of the geofence. This geofence gets automatically
// removed after this period of time.
.setExpirationDuration(sp.getFloat("geofenceTime", 1.0f).toLong() * 60 * 60 * 1000)

// Set the transition types of interest. Alerts are only generated for these
// transition. We track entry and exit transitions in this sample.
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)

// Create the geofence.
.build())

geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent).run {
addOnSuccessListener {
Log.e(TAG, "addOnSuccessListener")
showSnackbar(this@MainComposeActivity,
R.string.add_geofences,
R.string.geofences_added,
View.OnClickListener {
var dbHandler = DBHandler.open(this@MainComposeActivity)
var geoBean = GeoDataBean(
0, documents.placeName, documents.posY, documents.posX, documents.addressName, "Y"
)
dbHandler.insert(geoBean)
dbHandler.close()
}
)
}
addOnFailureListener {
// Failed to add geofences
// ...
Log.e(TAG, "addOnFailureListener ${it.message}")
}
}
}

 


이상으로 구현 예시는 정리를 마치도록 하겠습니다.  이렇게 구현 되는 앱은 다 정리가 되면 playstore에 게시할 예정입니다.   





오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

이런 날도 오네요.   처음이에요...






오늘의 이야기

private val geofencePendingIntent: PendingIntent by lazy {
val intent = Intent(this, GeofenceBroadcastReceiver::class.java)
intent.action = ACTION_GEOFENCE_EVENT
PendingIntent.getBroadcast(this, 0, intent, FLAG_MUTABLE)
}

지오펜스는실제 지리적 영역에 대한 가상 경계 경계입니다.[1] 지오펜스는 동적으로 생성되거나(포인트 위치 주변의 반경에서와 같이) 미리 정의된 경계 세트(예: 스쿨 존 또는 이웃 경계)와 일치할 수 있습니다.


지오펜스의 사용은 지오펜싱이라고 하며 , 사용의 한 예는 지오펜스에 들어가거나 나가는 위치 기반 서비스 (LBS) 사용자의 위치 인식 장치를 포함합니다. 이 활동은 장치 사용자에 대한 경고와 지오펜스 운영자에게 메시지를 보낼 수 있습니다. 장치의 위치를 ​​포함할 수 있는 이 정보는 휴대폰 이나 이메일 계정으로 전송될 수 있습니다.


 


이 글은 https://en.wikipedia.org/wiki/Geo-fence에서 퍼온 번역글입니다. 



 


Geo-fence - Wikipedia


Virtual perimeter Two geofences defined in a GPS application A geofence is a virtual perimeter for a real-world geographic area.[1] A geofence could be dynamically generated (as in a radius around a point location) or match a predefined set of boundaries (


en.wikipedia.org




 


해서 나는 오늘도 지오펜싱에 대한 오해(?)를 하고 있었습니다.  위에서 기술된 소스는 geofence 수신을 설정하기 구현하는 예제를 android codelab에서 보면서 따라 하기를 하면서 구현한 소스이기도 합니다 


 


android 가 API 13을 위해서 퍼블리싱을 해야 하는 경우 경고를 날리는(?) 부분이 있습니다. 어디냐 하면 PendingIntent을 broadcast 하는 경우 따라가는 파라미터 중 FLAG_IMMUTABLE을 서술하라고 하는 경우 입니다. gradle build 을 하는 경우 에러 메시지가 나타나고 가이드 문구에 FLAG_IMMUTABLE 을 넣으라고 안내를 합니다. 


 


그래서 난 착하게(?) 그대로 따라 해 보았습니다.  그런데... 그것 때문인지 아직 잘 알지는 못하지만, 지오펜싱을 수신할 때


진입, 이탈하는 것을 결괏값으로 수신하여야 하나, 그것이 잘 되지 않는 문제가 발생했습니다.  아래 소스 예시 중에서  Geofence.GEOFENCE_TRANSITION_DWELL, GEOFENCE_TRANSITION_ENTER, GEOFENCE_TRANSITION_EXIT 등의 결과 구분을 못하는 경우를 말합니다. 그래서 구현하고자 했던 기능 구현이 어렵게 되는 현상이 있었습니다. 


 


<GeofenceBroadcastReceiver 소스의 일부 발췌>


override fun onReceive(context: Context, intent: Intent) {

if (intent.action == ACTION_GEOFENCE_EVENT) {
Log.e(TAG, "onReceive ...")

val geofencingEvent = GeofencingEvent.fromIntent(intent)
// Test that the reported transition was of interest.
if (geofencingEvent != null) {

if (geofencingEvent.hasError()) {
val errorMessage = GeofenceStatusCodes.getStatusCodeString(geofencingEvent.errorCode)
Log.e("GeofenceBR", errorMessage)
return
}

when(val geofenceTransition = geofencingEvent.geofenceTransition) {
Geofence.GEOFENCE_TRANSITION_DWELL -> {
if (context != null) {
sendNotification(context, "GeoFence Dwell")
}
Log.e(TAG, context.getString(R.string.DWell))
}
Geofence.GEOFENCE_TRANSITION_ENTER -> {
if (context != null) {
sendNotification(context, context.getString(R.string.Enter))
}
Log.e(TAG, "Entered")
}
Geofence.GEOFENCE_TRANSITION_EXIT -> {
if (context != null) {
sendNotification(context, context.getString(R.string.Exit))
}
Log.e(TAG, "Exit")
}
else -> {
if (context != null) {
sendNotification(context, "Geofence ${geofenceTransition.hashCode()}")
}
Log.e(TAG, "geofenceTransition = ${geofenceTransition.hashCode()}")
}

}
} else {
Log.e(TAG, "geofencingEvent is null...")
}
}
}

 


그래서 이런저런 검색을 하던 중에 FLAG_MUTABLE을 파라미터로 적용하는 것을 알려주는 stackoverflow.com의 게시글을 하나 발견했습니다.  해서 현재 구현된 앱을 이용하여 테스트를 진행해 보고 있습니다.


 


오늘이 1일 차 테스트 중이라 아직 확실한 결과를 도출하였다고 하기 힘들 것 같아, 이번 주 중으로 결과를 알게 되면 다시 


수정해 보겠습니다. 





오늘의 이야기


#스하리1000명프로젝트,
कभी-कभी विदेशी कामगारों से बात करना कठिन होता है, है न?
मैंने एक सरल ऐप बनाया है जो मदद करता है! आप अपनी भाषा में लिखते हैं, और दूसरे इसे अपनी भाषा में देखते हैं।
यह सेटिंग्स के आधार पर स्वचालित अनुवाद करता है।
आसान चैट के लिए बहुत उपयोगी। जब मौका मिले तो देख लेना!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

 


애드센스 입금내역



이런 날이 오네요... 시작은 미미 하기는 하나 그 끝은 창대하리라는 글귀처럼 다음엔 더 좋아질 것이라 믿으며... 오늘을 기록해 둡니다.  먼 뒷날을 위한 일이기는 하나 이런 소소한(?) 당근이라도 있어야 하는 게 아닐까 하는 생각이 들기도 합니다.  


 


처음 받는 거라 이제서야 알게 된 것은 카카오 뱅크는 $100 미만이라 수수료가 면제되었으나, 송금하는 쪽에서는 $10이나 때어갔네요. 흠... 뭐 아무튼 그걸로 제세금(?)은 낸 것으로 치면 되지 않을까 하는 생각이 들기는 합니다.


 


또 다른 날이 오기를 바라며...





오늘의 이야기

새이름은 뭘까요?



 


오늘 나타난 바탕 화면 이미지 입니다. 한쌍의 새가 떠~억 나왔습니다. 


 


이 새의 이름은 무엇인지 아시나요? 저는 모르겠습니다.


 


아무튼 한쌍의 새들과 같이 쭈~욱 행복한 날들이 함께 하였으면 합니다. 


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 श्नीडल, बैडमिंटन क्लबों के लिए एक आवश्यक ऐप!
👉 मैच खेलें - स्कोर रिकॉर्ड करें और विरोधियों को खोजें 🎉
कहीं भी, अकेले, दोस्तों के साथ, या क्लब में बिल्कुल सही! 🤝
अगर आपको बैडमिंटन पसंद है तो इसे जरूर ट्राई करें

ऐप पर जाएं 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

오늘은 읽었던 인터넷 정보 중에서 하나 정리를 해 볼까 합니다.  원본 출처는 아래 링크를 참고하세요.


https://farhan-tanvir.medium.com/10-useful-android-studio-intellij-idea-plugins-to-make-work-eas ier-61d0ab4c5879



 


10 Useful Android Studio- IntelliJ IDEA Plugins to Make Work Easier


Power up your IDEs


farhan-tanvir.medium.com




원본 출처의 글 중에서 눈에 가는 것은 json string을 이용해서 직접 코드를 생성해 가는 것입니다.  개발을 하다 보면 json 데이터를 받아와서 데이터로 활용해야 하는 경우가 많습니다. 지금 하고 있는 작업 중에도 그런 부분이 있고요. opinet에서 받아오는 API 정보를 활용하는 코드 작업을 하는 동안에도 유용하게 사용이 될 것 같아서 정리를 해 두려고 합니다. 


 


https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-



 


JSON To Kotlin Class (JsonToKotlinClass) - IntelliJ IDEs Plugin | Marketplace


Plugin for Kotlin to convert Json String into Kotlin data class code quickly.


plugins.jetbrains.com




 


plugin market에서는 위 글을 참고해 보시면 될 것 같습니다.  먼저 생성된 코드를 보도록 하겠습니다


 


import com.google.gson.annotations.SerializedName


data class AvgRecentPriceBean(
@SerializedName("RESULT")
var result: RESULT = RESULT()
)

data class RESULT(
@SerializedName("OIL")
var oil: List<OIL> = listOf()
)

data class OIL(
@SerializedName("DATE")
var date: String = "",
@SerializedName("PRICE")
var price: Double = 0.0,
@SerializedName("PRODCD")
var prodcd: String = ""
)

다만 생성된 코드에서 수정한 부분은 변수 명칭이 조금 보기가 좋지 않습니다. 해서 저는 생성된 변수 이름을 전부다 소문자로 수정했습니다. 


 


이제 생성하는 순서를 보도록 하겠습니다. 먼저 plugin 을 설치해야 하기 위해서 android studio의 File 메뉴에서 Setting을 들어가 보겠습니다.  그 안에서 Plugins을 찾아봅니다. 


setting 에서 plugin 찾아보기



plugins에서 json을 입력하고 검색을 하면 바로 나오더군요. 그래서 Json To Kotlin Class을 설치하였습니다. 


 


json string 을 변환해 보기



먼저 opinet에서 수신받은 json string을 변환해 봅니다.  실행하는 방법은 alt-K (메뉴에서는 Code - Generate 을 선택했을 때 나오는 팝업메뉴에 Kotlin Data Class from Json을 선택 합니다. ) 을 입력 하면 위 그림과 같이 팝업이 나옵니다. 그러면 json string 을 안에 입력하고 format 버튼을 클릭하면 위 그림과 같이 정렬된 json string 을 볼 수 있습니다.


 


SerializedName Annotation을 넣기 위해서 선택하기



그다음은 아래 왼쪽에 있는 Advanced 버튼을 클릭하고 2번째 있는 Annotation을 선택합니다. opinet API을 호출하는 방식을 retrofit을 이용하여 json format을 수신을 하기 위해서 Gson을 선택했습니다.


 


그리고 주의할 것은 Class 파일을 먼저 생성한 다음 진행해야 합니다. 그렇지 않을 경우 현재 수정 중인 class 소스에 그냥 넣어 주기 때문입니다. 이제 생성할 class 이름을 입력하고 Generate을 실행하면 처음에 보여 드렸던 것과 같은 source code을 얻을 수 있습니다.  이제부터는 dataBean 구현을 위해서 code 입력을 줄여볼 수 있을 것 같습니다. 


 


 





오늘의 이야기

빋드 오류



앱을 개발하는 동안 이런 오류가 나타납니다. gradle 파일에 선언된 implementation을 살펴보아도 lifecycle 과 연관된 것은 선언된 내용이 없습니다. 같은 class 내용이 여기저기에 있다는 내용이고, 작성한 코드가 아닌 참조하는 것들 중에서 중복이 나타난다는 의미입니다.

이런 경우는 아예 gradle 파일에 설정을 해 주는 것으로 해소를 했습니다.


implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"


현재는 이 버전에 최신 이기 때문에 이렇게 한 줄 추가해 주는 것으로 해소를 했습니다만, 최신 버전은 그때 그때 확인을 해 주셔야 할 것 같습니다.


빌드 끝



 


2022.10.02 또 한가지 다른 이유를 알게 되어 수정 합니다. 


https://developer.android.com/jetpack/androidx/migrate?hl=ko 



 


AndroidX로 이전  |  Android 개발자  |  Android Developers


AndroidX로 이전 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. AndroidX는 원래 지원 라이브러리 API를 androidx 네임스페이스의 패키지로 대체합니다. 패키지 및 M


developer.android.com




 위 링크에서 아래와 같은 설명이 있습니다 .gradle.properties 에 아래 옵션을 추가 하는 방법 입니다.


Android 스튜디오를 사용해 기존 프로젝트 이전


Android 스튜디오 3.2 이상에서는 메뉴 바에서 Refactor > Migrate to AndroidX를 선택하여 기존 프로젝트를 AndroidX로 이전할 수 있습니다.


리팩토링 명령은 두 개의 플래그를 사용합니다. 기본적으로 둘 다 gradle.properties 파일에서 true로 설정되어 있습니다.


 


android.useAndroidX=true


Android 플러그인은 지원 라이브러리 대신 적절한 AndroidX 라이브러리를 사용합니다.


 


android.enableJetifier=true


Android 플러그인은 바이너리를 다시 작성해 기존 타사 라이브러리를 자동으로 이전하여 AndroidX를 사용합니다.


 


설정하고 나서 signed bundle 이 build 된 결과 예시



 





오늘의 이야기


#스하리1000명프로젝트,
¿Perdido en Corea? Incluso si no hablas coreano, esta aplicación te ayuda a moverte fácilmente.
Simplemente hable su idioma: traduce, busca y muestra resultados en su idioma.
¡Genial para viajeros! Admite más de 10 idiomas, incluidos inglés, japonés, chino, vietnamita y más.
¡Pruébalo ahora!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




오늘의 이야기

인앱결제 코드 이전 이야기 이전 버전에서는 java 코드로 구현된 소스 코드를 공유해 보았습니다.   https://billcorea.tistory.com/165   안드로이드 앱 만들기 : 구글 인앱 결제 쉽게 따라하기 (정기결제, 소스공유) 이전 ...