초기 관제 시스템 뼈대 구성
This commit is contained in:
29
apps/android/README.md
Normal file
29
apps/android/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# CargoRadar Android
|
||||
|
||||
This is the first driver-app skeleton. It uses a foreground service, Android's
|
||||
native `LocationManager`, and `HttpURLConnection` so the MVP has no runtime
|
||||
library dependencies beyond the Android Gradle/Kotlin plugins.
|
||||
|
||||
## Build
|
||||
|
||||
Open `apps/android` in Android Studio, sync Gradle, then run the `app` module.
|
||||
|
||||
Current Gradle plugin choices:
|
||||
|
||||
- Android Gradle Plugin: `9.2.0`
|
||||
- Kotlin: `2.4.0`
|
||||
- compile/target SDK: `36`
|
||||
|
||||
## MVP behavior
|
||||
|
||||
- The app asks for foreground location and notification permissions.
|
||||
- Press `운행 시작` to start a location foreground service.
|
||||
- The service uploads location JSON to `/api/v1/locations`.
|
||||
- Failed uploads are queued locally and retried after the next successful send.
|
||||
- Press `운행 종료` to stop location updates.
|
||||
|
||||
For emulator testing, keep the default server URL `http://10.0.2.2:8080`.
|
||||
For a real phone, use the NAS or PC LAN URL, for example
|
||||
`http://192.168.0.10:8080`.
|
||||
|
||||
Production builds should use HTTPS and disable cleartext traffic.
|
||||
21
apps/android/app/build.gradle.kts
Normal file
21
apps/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.comtropy.cargoradar"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.comtropy.cargoradar"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
33
apps/android/app/src/main/AndroidManifest.xml
Normal file
33
apps/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".LocationUploadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.comtropy.cargoradar
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Base64
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.time.Instant
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class LocationUploadService : Service(), LocationListener {
|
||||
private val uploadExecutor = Executors.newSingleThreadExecutor()
|
||||
private lateinit var locationManager: LocationManager
|
||||
|
||||
private var serverUrl: String = ""
|
||||
private var driverId: String = ""
|
||||
private var vehicleNo: String = ""
|
||||
private var deviceToken: String = ""
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
stopTracking()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
ACTION_START -> {
|
||||
serverUrl = intent.getStringExtra(EXTRA_SERVER_URL).orEmpty().trimEnd('/')
|
||||
driverId = intent.getStringExtra(EXTRA_DRIVER_ID).orEmpty()
|
||||
vehicleNo = intent.getStringExtra(EXTRA_VEHICLE_NO).orEmpty()
|
||||
deviceToken = intent.getStringExtra(EXTRA_DEVICE_TOKEN).orEmpty()
|
||||
|
||||
startAsForeground("Waiting for location")
|
||||
startLocationUpdates()
|
||||
return START_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
stopTracking()
|
||||
uploadExecutor.shutdown()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location) {
|
||||
updateNotification("Uploading location · ${location.provider}")
|
||||
uploadLocation(location)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startLocationUpdates() {
|
||||
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
updateNotification("Location permission required")
|
||||
return
|
||||
}
|
||||
|
||||
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||
.filter { provider -> locationManager.isProviderEnabled(provider) }
|
||||
|
||||
if (providers.isEmpty()) {
|
||||
updateNotification("No location provider enabled")
|
||||
return
|
||||
}
|
||||
|
||||
for (provider in providers) {
|
||||
locationManager.requestLocationUpdates(
|
||||
provider,
|
||||
MIN_UPDATE_INTERVAL_MS,
|
||||
MIN_UPDATE_DISTANCE_M,
|
||||
this
|
||||
)
|
||||
|
||||
locationManager.getLastKnownLocation(provider)?.let(::uploadLocation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTracking() {
|
||||
locationManager.removeUpdates(this)
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun uploadLocation(location: Location) {
|
||||
if (serverUrl.isBlank() || driverId.isBlank() || deviceToken.isBlank()) {
|
||||
updateNotification("Server config required")
|
||||
return
|
||||
}
|
||||
|
||||
val endpoint = "$serverUrl/api/v1/locations"
|
||||
val payload = buildLocationPayload(location)
|
||||
|
||||
uploadExecutor.execute {
|
||||
val sent = sendPayload(endpoint, payload)
|
||||
|
||||
if (sent) {
|
||||
flushQueuedLocations(endpoint)
|
||||
updateNotification("Last upload ok · ${Instant.now()}")
|
||||
} else {
|
||||
enqueueLocationPayload(payload)
|
||||
updateNotification("Upload queued · ${queuedPayloads().size}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildLocationPayload(location: Location): String {
|
||||
val speedKmh = if (location.hasSpeed()) location.speed.toDouble() * 3.6 else null
|
||||
val heading = if (location.hasBearing()) location.bearing.toDouble() else null
|
||||
val accuracy = if (location.hasAccuracy()) location.accuracy.toDouble() else null
|
||||
val recordedAt = Instant.ofEpochMilli(location.time).toString()
|
||||
|
||||
return "{" +
|
||||
"\"driverId\":${json(driverId)}," +
|
||||
"\"vehicleNo\":${json(vehicleNo)}," +
|
||||
"\"latitude\":${number(location.latitude)}," +
|
||||
"\"longitude\":${number(location.longitude)}," +
|
||||
"\"accuracy\":${nullableNumber(accuracy)}," +
|
||||
"\"speed\":${nullableNumber(speedKmh)}," +
|
||||
"\"heading\":${nullableNumber(heading)}," +
|
||||
"\"provider\":${json(location.provider.orEmpty())}," +
|
||||
"\"recordedAt\":${json(recordedAt)}" +
|
||||
"}"
|
||||
}
|
||||
|
||||
private fun sendPayload(endpoint: String, payload: String): Boolean {
|
||||
return try {
|
||||
val connection = (URL(endpoint).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
connectTimeout = 10000
|
||||
readTimeout = 10000
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json; charset=utf-8")
|
||||
setRequestProperty("X-Device-Token", deviceToken)
|
||||
}
|
||||
|
||||
connection.outputStream.use { stream ->
|
||||
stream.write(payload.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
connection.disconnect()
|
||||
responseCode in 200..299
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushQueuedLocations(endpoint: String) {
|
||||
val queued = queuedPayloads()
|
||||
if (queued.isEmpty()) return
|
||||
|
||||
val remaining = mutableListOf<String>()
|
||||
|
||||
for (payload in queued) {
|
||||
if (!sendPayload(endpoint, payload)) {
|
||||
remaining.add(payload)
|
||||
}
|
||||
}
|
||||
|
||||
saveQueuedPayloads(remaining)
|
||||
}
|
||||
|
||||
private fun enqueueLocationPayload(payload: String) {
|
||||
val queued = queuedPayloads().toMutableList()
|
||||
queued.add(payload)
|
||||
|
||||
while (queued.size > MAX_QUEUED_LOCATIONS) {
|
||||
queued.removeAt(0)
|
||||
}
|
||||
|
||||
saveQueuedPayloads(queued)
|
||||
}
|
||||
|
||||
private fun queuedPayloads(): List<String> {
|
||||
val encoded = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)
|
||||
.getString(KEY_QUEUED_PAYLOADS, "")
|
||||
.orEmpty()
|
||||
|
||||
if (encoded.isBlank()) return emptyList()
|
||||
|
||||
return encoded
|
||||
.lineSequence()
|
||||
.filter { it.isNotBlank() }
|
||||
.mapNotNull { line ->
|
||||
try {
|
||||
String(Base64.decode(line, Base64.NO_WRAP), Charsets.UTF_8)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun saveQueuedPayloads(payloads: List<String>) {
|
||||
val encoded = payloads.joinToString("\n") { payload ->
|
||||
Base64.encodeToString(payload.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(KEY_QUEUED_PAYLOADS, encoded)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun startAsForeground(content: String) {
|
||||
val notification = buildNotification(content)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotification(content: String) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.notify(NOTIFICATION_ID, buildNotification(content))
|
||||
}
|
||||
|
||||
private fun buildNotification(content: String): Notification {
|
||||
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Notification.Builder(this, CHANNEL_ID)
|
||||
} else {
|
||||
Notification.Builder(this)
|
||||
}
|
||||
|
||||
return builder
|
||||
.setSmallIcon(R.drawable.ic_stat_location)
|
||||
.setContentTitle(getString(R.string.notification_title))
|
||||
.setContentText(content)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun json(value: String): String {
|
||||
return "\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\""
|
||||
}
|
||||
|
||||
private fun number(value: Double): String {
|
||||
return String.format(Locale.US, "%.7f", value)
|
||||
}
|
||||
|
||||
private fun nullableNumber(value: Double?): String {
|
||||
return value?.let { String.format(Locale.US, "%.3f", it) } ?: "null"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "com.comtropy.cargoradar.action.START"
|
||||
const val ACTION_STOP = "com.comtropy.cargoradar.action.STOP"
|
||||
const val EXTRA_SERVER_URL = "server_url"
|
||||
const val EXTRA_DRIVER_ID = "driver_id"
|
||||
const val EXTRA_VEHICLE_NO = "vehicle_no"
|
||||
const val EXTRA_DEVICE_TOKEN = "device_token"
|
||||
|
||||
private const val CHANNEL_ID = "cargo_radar_location"
|
||||
private const val NOTIFICATION_ID = 1201
|
||||
private const val MIN_UPDATE_INTERVAL_MS = 30_000L
|
||||
private const val MIN_UPDATE_DISTANCE_M = 100f
|
||||
private const val MAX_QUEUED_LOCATIONS = 500
|
||||
private const val PREFERENCES_NAME = "cargo_radar_location_service"
|
||||
private const val KEY_QUEUED_PAYLOADS = "queued_payloads"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.comtropy.cargoradar
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.InputType
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainActivity : Activity() {
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private lateinit var serverUrlInput: EditText
|
||||
private lateinit var driverIdInput: EditText
|
||||
private lateinit var vehicleNoInput: EditText
|
||||
private lateinit var tokenInput: EditText
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
preferences = getSharedPreferences("cargo_radar_config", MODE_PRIVATE)
|
||||
setContentView(buildContentView())
|
||||
}
|
||||
|
||||
private fun buildContentView(): View {
|
||||
val container = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(20), dp(18), dp(20), dp(18))
|
||||
}
|
||||
|
||||
val title = TextView(this).apply {
|
||||
text = getString(R.string.app_name)
|
||||
textSize = 24f
|
||||
setTextColor(0xff17212b.toInt())
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
container.addView(title, matchWrap())
|
||||
|
||||
val subtitle = TextView(this).apply {
|
||||
text = "운행 위치 업로드"
|
||||
textSize = 14f
|
||||
setTextColor(0xff657487.toInt())
|
||||
}
|
||||
container.addView(subtitle, marginBottom(20))
|
||||
|
||||
serverUrlInput = input(
|
||||
key = "server_url",
|
||||
defaultValue = "http://10.0.2.2:8080",
|
||||
hint = "서버 URL"
|
||||
)
|
||||
driverIdInput = input("driver_id", "demo-driver", "기사 ID")
|
||||
vehicleNoInput = input("vehicle_no", "SEOUL-12-3456", "차량 번호")
|
||||
tokenInput = input("device_token", "demo-token", "기기 토큰")
|
||||
|
||||
addField(container, "서버 URL", serverUrlInput)
|
||||
addField(container, "기사 ID", driverIdInput)
|
||||
addField(container, "차량 번호", vehicleNoInput)
|
||||
addField(container, "기기 토큰", tokenInput)
|
||||
|
||||
val permissionButton = Button(this).apply {
|
||||
text = "권한 확인"
|
||||
setOnClickListener { requestRuntimePermissions() }
|
||||
}
|
||||
container.addView(permissionButton, marginTop(8))
|
||||
|
||||
val startButton = Button(this).apply {
|
||||
text = "운행 시작"
|
||||
setOnClickListener { startTracking() }
|
||||
}
|
||||
container.addView(startButton, marginTop(8))
|
||||
|
||||
val stopButton = Button(this).apply {
|
||||
text = "운행 종료"
|
||||
setOnClickListener { stopTracking() }
|
||||
}
|
||||
container.addView(stopButton, marginTop(8))
|
||||
|
||||
val settingsButton = Button(this).apply {
|
||||
text = "앱 위치 설정"
|
||||
setOnClickListener {
|
||||
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = android.net.Uri.parse("package:$packageName")
|
||||
})
|
||||
}
|
||||
}
|
||||
container.addView(settingsButton, marginTop(8))
|
||||
|
||||
statusText = TextView(this).apply {
|
||||
text = "대기 중"
|
||||
textSize = 14f
|
||||
setTextColor(0xff17212b.toInt())
|
||||
}
|
||||
container.addView(statusText, marginTop(18))
|
||||
|
||||
return ScrollView(this).apply {
|
||||
addView(container)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addField(container: LinearLayout, labelText: String, input: EditText) {
|
||||
val label = TextView(this).apply {
|
||||
text = labelText
|
||||
textSize = 13f
|
||||
setTextColor(0xff657487.toInt())
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
container.addView(label, marginTop(14))
|
||||
container.addView(input, matchWrap())
|
||||
}
|
||||
|
||||
private fun input(key: String, defaultValue: String, hint: String): EditText {
|
||||
return EditText(this).apply {
|
||||
setText(preferences.getString(key, defaultValue))
|
||||
this.hint = hint
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
singleLine = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestRuntimePermissions() {
|
||||
val permissions = mutableListOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
requestPermissions(permissions.toTypedArray(), REQUEST_PERMISSIONS)
|
||||
}
|
||||
|
||||
private fun startTracking() {
|
||||
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestRuntimePermissions()
|
||||
statusText.text = "위치 권한이 필요합니다."
|
||||
return
|
||||
}
|
||||
|
||||
saveConfig()
|
||||
|
||||
val intent = Intent(this, LocationUploadService::class.java).apply {
|
||||
action = LocationUploadService.ACTION_START
|
||||
putExtra(LocationUploadService.EXTRA_SERVER_URL, serverUrlInput.text.toString().trim())
|
||||
putExtra(LocationUploadService.EXTRA_DRIVER_ID, driverIdInput.text.toString().trim())
|
||||
putExtra(LocationUploadService.EXTRA_VEHICLE_NO, vehicleNoInput.text.toString().trim())
|
||||
putExtra(LocationUploadService.EXTRA_DEVICE_TOKEN, tokenInput.text.toString().trim())
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
|
||||
statusText.text = "위치 업로드를 시작했습니다."
|
||||
}
|
||||
|
||||
private fun stopTracking() {
|
||||
val intent = Intent(this, LocationUploadService::class.java).apply {
|
||||
action = LocationUploadService.ACTION_STOP
|
||||
}
|
||||
startService(intent)
|
||||
statusText.text = "위치 업로드를 종료했습니다."
|
||||
}
|
||||
|
||||
private fun saveConfig() {
|
||||
preferences.edit()
|
||||
.putString("server_url", serverUrlInput.text.toString().trim())
|
||||
.putString("driver_id", driverIdInput.text.toString().trim())
|
||||
.putString("vehicle_no", vehicleNoInput.text.toString().trim())
|
||||
.putString("device_token", tokenInput.text.toString().trim())
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == REQUEST_PERMISSIONS && grantResults.any { it == PackageManager.PERMISSION_GRANTED }) {
|
||||
statusText.text = "권한 확인 완료"
|
||||
}
|
||||
}
|
||||
|
||||
private fun matchWrap(): LinearLayout.LayoutParams {
|
||||
return LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
private fun marginTop(topDp: Int): LinearLayout.LayoutParams {
|
||||
return matchWrap().apply {
|
||||
topMargin = dp(topDp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun marginBottom(bottomDp: Int): LinearLayout.LayoutParams {
|
||||
return matchWrap().apply {
|
||||
bottomMargin = dp(bottomDp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dp(value: Int): Int {
|
||||
return (value * resources.displayMetrics.density).roundToInt()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_PERMISSIONS = 1001
|
||||
}
|
||||
}
|
||||
10
apps/android/app/src/main/res/drawable/ic_stat_location.xml
Normal file
10
apps/android/app/src/main/res/drawable/ic_stat_location.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,2C8.14,2 5,5.14 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.86 -3.14,-7 -7,-7zM12,12.5c-1.38,0 -2.5,-1.12 -2.5,-2.5S10.62,7.5 12,7.5s2.5,1.12 2.5,2.5S13.38,12.5 12,12.5z" />
|
||||
</vector>
|
||||
6
apps/android/app/src/main/res/values/strings.xml
Normal file
6
apps/android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">CargoRadar Driver</string>
|
||||
<string name="notification_title">CargoRadar 운행 추적</string>
|
||||
<string name="notification_channel_name">운행 위치 업로드</string>
|
||||
</resources>
|
||||
9
apps/android/app/src/main/res/values/styles.xml
Normal file
9
apps/android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:statusBarColor">#FFFFFF</item>
|
||||
<item name="android:navigationBarColor">#FFFFFF</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
apps/android/build.gradle.kts
Normal file
4
apps/android/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.application") version "9.2.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.4.0" apply false
|
||||
}
|
||||
18
apps/android/settings.gradle.kts
Normal file
18
apps/android/settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "CargoRadarAndroid"
|
||||
include(":app")
|
||||
Reference in New Issue
Block a user