Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions .github/workflows/deploy_on_apk.text

This file was deleted.

87 changes: 87 additions & 0 deletions .github/workflows/deploy_on_apk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Android Build and Deploy

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop

jobs:
build-android-app:
name: Build Android APK
runs-on: ubuntu-latest

steps:
- name: Checkout source code
uses: actions/checkout@v2

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew

- name: Create google-services.json directory
run: mkdir -p ./mobile/src/main # ์ƒ๋Œ€ ๊ฒฝ๋กœ ์‚ฌ์šฉ ๋ฐ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ ๋ณด์žฅ

- name: Restore google-services.json from secret
run: |
echo "Attempting to write google-services.json..."
# ์ ˆ๋Œ€ ๊ฒฝ๋กœ ๋Œ€์‹  ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.
# GitHub Actions์˜ working-directory๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ฒดํฌ์•„์›ƒ๋œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค.
TARGET_FILE=""

# Secret ๋‚ด์šฉ์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ (๋””๋ฒ„๊น…)
if [ -z "${{ secrets.GOOGLE_SERVICE_JSON }}" ]; then
echo "Error: Secret GOOGLE_SERVICE_JSON is empty or not set."
exit 1
fi

echo "${{ secrets.GOOGLE_SERVICE_JSON }}" > ./mobile/src/main/google-services.json

echo "google-services.json created. Verifying file:"
ls -l ./mobile/src/main/google-services.json # ํŒŒ์ผ ์กด์žฌ ๋ฐ ํฌ๊ธฐ ํ™•์ธ
echo "First 5 lines of google-services.json:"
head -n 5 ./mobile/src/main/google-services.json # ํŒŒ์ผ ๋‚ด์šฉ ์•ž๋ถ€๋ถ„ ํ™•์ธ (Secret ๋‚ด์šฉ ๋…ธ์ถœ ์ฃผ์˜, ๋””๋ฒ„๊น… ํ›„ ๋ฏผ๊ฐํ•˜๋ฉด cat ์ „์ฒด ๋Œ€์‹  head ์‚ฌ์šฉ)
# ํŒŒ์ผ์ด ๋น„์–ด์žˆ์ง€ ์•Š์€์ง€ ํ™•์ธ
if [ ! -s ./mobile/src/main/google-services.json ]; then
echo "Error: ./mobile/src/main/google-services.json is empty after writing from secret."
exit 1
fi
# shell: bash # ๋ช…์‹œ์ ์œผ๋กœ bash ์‚ฌ์šฉ (๊ธฐ๋ณธ๊ฐ’์ด์ง€๋งŒ)

# IOT ์ธ์ฆ์„œ ํŒŒ์ผ ์ƒ์„ฑ ๋ถ€๋ถ„ (๊ธฐ์กด๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋˜, ๊ฒฝ๋กœ ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ƒ๋Œ€๊ฒฝ๋กœ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ)
- name: Write PEM files from secrets
run: |
mkdir -p ./mobile/src/main/assets

# IOT Certificate
if [ -z "${{ secrets.IOT_CERTIFICATE }}" ]; then echo "Error: Secret IOT_CERTIFICATE is empty."; exit 1; fi
echo "${{ secrets.IOT_CERTIFICATE }}" > ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-certificate.pem.crt
chmod 600 ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-certificate.pem.crt
ls -l ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-certificate.pem.crt

# Private PEM
if [ -z "${{ secrets.PRIVATE_PEM }}" ]; then echo "Error: Secret PRIVATE_PEM is empty."; exit 1; fi
echo "${{ secrets.PRIVATE_PEM }}" > ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-private.pem.crt
chmod 600 ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-private.pem.crt
ls -l ./mobile/src/main/assets/c060311b74ab5d78e0c9918acc72ebeb07d4d48accfab78d7296dae0e3718872-private.pem.crt

- name: Build Debug APK
# ๋ชจ๋“ˆ ์ด๋ฆ„์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ๋” ์ •ํ™•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
# ์˜ˆ: ./gradlew :mobile:assembleDebug ๋˜๋Š” ./gradlew mobile:assembleDebug
run: ./gradlew :mobile:assembleDebug # ๋˜๋Š” ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ์— ๋งž๊ฒŒ ์ˆ˜์ •

- name: Upload Debug APK
uses: actions/upload-artifact@v4
with:
name: debug-apk
# ๋ชจ๋“ˆ ์ด๋ฆ„์ด 'mobile'์ด๋ฏ€๋กœ ๊ฒฝ๋กœ๋„ mobile๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.
path: mobile/build/outputs/apk/debug/mobile-debug.apk # ์‹ค์ œ APK ๊ฒฝ๋กœ ํ™•์ธ ํ•„์š”
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ private const val BIO_DATA_PATH = "/bio_data" // ์ƒ์ฒด ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ
private const val MOBILE_PREFS_NAME = "MobileAppPrefs"
private const val KEY_STORED_WEARABLE_ID = "storedWearableDeviceId" // ๋ชจ๋ฐ”์ผ ์•ฑ์— ์ €์žฅํ•  ์›จ์–ด๋Ÿฌ๋ธ” ID ํ‚ค


class WearableListener : WearableListenerService() {
private lateinit var workerInfoSender: WorkerInfoSender

override fun onDataChanged(dataEvents: DataEventBuffer) {
Log.d(TAG, "onDataChanged received ${dataEvents.count} events")
Expand Down Expand Up @@ -63,6 +65,7 @@ class WearableListener : WearableListenerService() {
if (wearableIdForTopic == null) {
Log.e(TAG, "Wearable ID not found in SharedPreferences. Cannot create MQTT topic.")
// ID๊ฐ€ ์—†์œผ๋ฉด MQTT ์ „์†ก์„ ์‹œ๋„ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜, ๊ธฐ๋ณธ ํ† ํ”ฝ ๋“ฑ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
workerInfoSender.requestWearableIdFromWear(this)
return@forEach // ๋˜๋Š” ๋‹ค๋ฅธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
}

Expand Down Expand Up @@ -111,4 +114,5 @@ class WearableListener : WearableListenerService() {
super.onDestroy()
Log.d(TAG, "WearableListenerService destroyed")
}

}
17 changes: 16 additions & 1 deletion mobile/src/main/java/com/f2r/mobile/worker/WorkerInfoSender.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.f2r.mobile.worker

import android.content.Context
import android.util.Log
import androidx.lifecycle.LifecycleService
import com.google.android.gms.common.api.ApiException
Expand All @@ -14,15 +15,29 @@ import kotlinx.coroutines.tasks.await
* ๋ชจ๋ฐ”์ผ์— ํ• ๋‹น๋œ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„์„ ์›Œ์น˜์— 1ํšŒ ํ‘ธ์‹œํ•ด ๋งคํ•‘ํ•œ๋‹ค.
* Hard-coded sample โ€“ replace with DB / API later.
*/

private const val REQUEST_WEARABLE_ID_PATH = "/request_wearable_id"

class WorkerInfoSender : LifecycleService() {

private val scope = CoroutineScope(Dispatchers.IO)
private val dataClient by lazy { Wearable.getDataClient(this) }

private val messageClient by lazy { Wearable.getMessageClient(this) }
override fun onCreate() {
super.onCreate()
scope.launch { pushWorkerInfo() }
}
fun requestWearableIdFromWear(context: Context) {
messageClient.sendMessage(
"wear_node_id", // Wear OS ๊ธฐ๊ธฐ์˜ ๋…ธ๋“œ ID (์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ์—์„œ ๊ฐ€์ ธ์˜ด)
REQUEST_WEARABLE_ID_PATH,
null // ๋ฉ”์‹œ์ง€์— ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์œผ๋ฉด null
).addOnSuccessListener {
Log.d("MobileApp", "Wearable ID request sent successfully.")
}.addOnFailureListener { e ->
Log.e("MobileApp", "Failed to send Wearable ID request.", e)
}
}

private suspend fun pushWorkerInfo() {
try {
Expand Down
75 changes: 69 additions & 6 deletions mobile/src/main/java/com/iot/myapplication/FCMService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,52 @@ import android.content.Context
import android.os.Build
import android.util.Log
import com.f2r.mobile.worker.WorkerInfo
import com.google.android.gms.wearable.CapabilityClient
import com.google.android.gms.wearable.CapabilityInfo
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.Node
import com.google.android.gms.wearable.NodeClient
import com.google.android.gms.wearable.Wearable
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.retrofit.FCMTokenRegistDto
import com.retrofit.RetrofitClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlin.collections.forEach


private const val TAG = "FCMService"
private const val DEFAULT_CHANNEL_ID = "Monitory"
private const val CHAT_CHANNEL_ID = "chat_channel"

// Wear OS ์•ฑ๊ณผ์˜ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ฒฝ๋กœ ๋ฐ Capability ์ •์˜
private const val WEAR_APP_CAPABILITY = "wear_app_capability" // Wear OS ์•ฑ์˜ AndroidManifest.xml์— ์ •์˜๋œ capability์™€ ์ผ์น˜ํ•ด์•ผ ํ•จ
private const val FCM_MESSAGE_PATH = "/fcm_message" // ๋ชจ๋ฐ”์ผ -> ์›จ์–ด๋Ÿฌ๋ธ” ๋ฉ”์‹œ์ง€ ๊ฒฝ๋กœ
class FCMService : FirebaseMessagingService() {
private lateinit var messageClient: MessageClient
private lateinit var capabilityClient: CapabilityClient
private lateinit var nodeClient: NodeClient

override fun onCreate() {
super.onCreate()
createNotificationChannels()
messageClient = Wearable.getMessageClient(this)
nodeClient = Wearable.getNodeClient(this)
capabilityClient = Wearable.getCapabilityClient(this)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Log.d(TAG, "Refreshed token: $token")
val workerId = WorkerInfo.toJson().getString("workerId")
CoroutineScope(Dispatchers.IO).launch {
RetrofitClient.instance.sendFCM(FCMTokenRegistDto(workerId, token))
try {
val workerId = WorkerInfo.toJson().getString("workerId")
CoroutineScope(Dispatchers.IO).launch {
RetrofitClient.instance.sendFCM(FCMTokenRegistDto(workerId, token))
}
} catch (e: Exception) {
Log.e(TAG, "Failed to get workerId for FCM token registration", e)
}
}

Expand All @@ -40,10 +63,50 @@ class FCMService : FirebaseMessagingService() {
// ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
val title = remoteMessage.notification?.title ?: remoteMessage.data["title"] ?: "์•Œ๋ฆผ"
val body = remoteMessage.notification?.body ?: remoteMessage.data["body"] ?: "์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค."
//
// val title = remoteMessage.data.get("title") ?: "์•Œ๋ฆผ"
// val body = remoteMessage.data.get("body") ?: "์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€๊ฐ€ ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค."

// 1. ๋ชจ๋ฐ”์ผ ์ž์ฒด์— ์•Œ๋ฆผ ํ‘œ์‹œ (๊ธฐ์กด ๋กœ์ง)
showNotification(title, body)

// 2. Wear OS ๊ธฐ๊ธฐ๋กœ ๋ฉ”์‹œ์ง€ ์ „์†ก
sendFcmDataToWearable(title, body)
}
private fun sendFcmDataToWearable(title: String, body: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val connectedNodesList = nodeClient.connectedNodes.await()
if (connectedNodesList.isEmpty()) {
Log.d(TAG, "NodeClient: No connected nodes found at all.")
} else {
Log.d(TAG, "NodeClient: Found ${connectedNodesList.size} connected node(s):")
connectedNodesList.forEach { node ->
Log.d(TAG, "Node: ${node.displayName}, ID: ${node.id}, isNearby: ${node.isNearby}")
}
}
if (connectedNodesList.isEmpty()) {
Log.d(TAG, "No wearable device with '$WEAR_APP_CAPABILITY' capability found or connected.")
return@launch
}

// ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ๋…ธ๋“œ์— ๋ฉ”์‹œ์ง€ ์ „์†ก (๋ณดํ†ต์€ ํ•˜๋‚˜์˜ ์›จ์–ด๋Ÿฌ๋ธ”๋งŒ ์—ฐ๊ฒฐ๋จ)
connectedNodesList.forEach { node ->
// ๋ฉ”์‹œ์ง€ ํŽ˜์ด๋กœ๋“œ ์ƒ์„ฑ (ByteArray๋กœ ๋ณ€ํ™˜)
// ๊ฐ„๋‹จํ•œ ๋ฌธ์ž์—ด ๊ฒฐํ•ฉ ๋˜๋Š” JSON/ProtoBuf ์‚ฌ์šฉ ๊ฐ€๋Šฅ
val payloadString = "$title|$body" // ๊ฐ„๋‹จํ•œ ๊ตฌ๋ถ„์ž ์‚ฌ์šฉ ์˜ˆ์‹œ
val payload: ByteArray = payloadString.toByteArray(Charsets.UTF_8)

messageClient.sendMessage(node.id, FCM_MESSAGE_PATH, payload)
.addOnSuccessListener {
Log.d(TAG, "FCM message sent to wearable (${node.displayName}): $payloadString")
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to send FCM message to wearable (${node.displayName})", e)
}
}
// ... ๊ธฐ์กด capabilityClient ๋กœ์ง ...
} catch (e: Exception) {
Log.e(TAG, "Error in NodeClient check or capability check", e)
}
}
}
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Expand Down
4 changes: 4 additions & 0 deletions mobile/src/main/java/com/iot/myapplication/Location.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.iot.myapplication

import com.google.gson.annotations.SerializedName

data class Location(
@SerializedName("zoneId")
val zoneId: String,
@SerializedName("zoneName")
val zoneName: String,
)
15 changes: 2 additions & 13 deletions mobile/src/main/java/com/iot/myapplication/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private val _mqttPublishStatus = MutableLiveData<String>()
val mqttPublishStatus: LiveData<String> = _mqttPublishStatus


init {
loadWorkerInfoDisplay()
initializeMqttClient()
fetchLocationFromServer()
fetchFcmToken()
// fetchFcmToken()
}

private fun initializeMqttClient(){
Expand Down Expand Up @@ -87,18 +88,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}

private fun fetchFcmToken() {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
if (task.isSuccessful) {
// FCM ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์„ฑ๊ณต
val token = task.result
Log.d("FCMService", "FCM Token: $token")
} else {
// FCM ํ† ํฐ ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ
Log.e("FCMService", "Failed to fetch FCM token", task.exception)
}
}
}
fun initializeAndStartService(){
viewModelScope.launch {
val serviceIntent = Intent(appContext, WorkerInfoSender::class.java)
Expand Down
8 changes: 6 additions & 2 deletions mobile/src/main/java/com/retrofit/RetrofitClient.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.retrofit

import com.google.gson.GsonBuilder
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
Expand All @@ -9,7 +10,7 @@ import software.amazon.awssdk.crt.BuildConfig

object RetrofitClient {
// ์‹ค์ œ ์šด์˜ ์‹œ์—๋Š” HTTPS URL ์‚ฌ์šฉ
private const val BASE_URL = "https://www.monitory.space/"
private const val BASE_URL = "https://api.monitory.space/"
// ๊ฐœ๋ฐœ ์‹œ ๋กœ์ปฌ HTTP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ์šฉ URL (์ฃผ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์„ ํƒ)
// private const val BASE_URL = "http://10.0.2.2:8080/" // ์—๋ฎฌ๋ ˆ์ดํ„ฐ์—์„œ ํ˜ธ์ŠคํŠธ PC์˜ localhost

Expand Down Expand Up @@ -46,11 +47,14 @@ object RetrofitClient {
}
}.build()

val gson = GsonBuilder()
.setLenient()
.create()
val instance: ApiService by lazy {
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
retrofit.create(ApiService::class.java)
}
Expand Down
Loading
Loading