Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ properties.load(project.rootProject.file('local.properties').newDataInputStream(

android {
namespace 'com.runnect.runnect'
compileSdk 34
compileSdk 35

defaultConfig {
applicationId "com.runnect.runnect"
minSdk 28
targetSdk 34
targetSdk 35
versionCode 22
versionName "2.0.1"

Expand Down
23 changes: 19 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<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.READ_EXTERNAL_STORAGE" />

<!-- 미디어 접근 권한 (API 레벨별 조건부) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<!-- Foreground Service 관련 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />

<!-- 알림 권한 (API 33+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- 정확한 알람 권한 (API 31+에서만 필요) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

<!-- 광고 ID 권한 (필요한 경우만) -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />

<!--queries에 카카오톡 패키지 추가-->
<queries>
Expand All @@ -32,7 +46,8 @@
<service
android:name=".presentation.run.TimerService"
android:enabled="true"
android:exported="true" />
android:exported="false"
android:foregroundServiceType="location" />

<activity
android:name=".presentation.login.GiveNicknameActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.viewbinding.ViewBinding
import com.runnect.runnect.util.EdgeToEdgeUtil

abstract class BindingActivity<B : ViewBinding>(@LayoutRes private val layoutResId: Int) :
AppCompatActivity() {
Expand All @@ -13,5 +14,8 @@ abstract class BindingActivity<B : ViewBinding>(@LayoutRes private val layoutRes
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, layoutResId)

// Edge-to-Edge 설정 (모든 BindingActivity에 적용)
EdgeToEdgeUtil.setupEdgeToEdge(this, binding.root)
}
}
5 changes: 3 additions & 2 deletions app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.runnect.runnect.data.service.*
import com.runnect.runnect.data.source.remote.*
import com.runnect.runnect.domain.*
import com.runnect.runnect.util.ApiLogger
import com.runnect.runnect.util.NetworkSecurityUtil
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -66,7 +67,7 @@ object RetrofitModule {
fun provideOkHttpClient(
logger: HttpLoggingInterceptor,
@Auth authInterceptor: Interceptor
): OkHttpClient = OkHttpClient.Builder()
): OkHttpClient = NetworkSecurityUtil.createSecureOkHttpClient()
.addInterceptor(logger)
.addInterceptor(authInterceptor)
.build()
Expand All @@ -78,7 +79,7 @@ object RetrofitModule {
logger: HttpLoggingInterceptor,
@Auth authInterceptor: Interceptor,
responseInterceptor: ResponseInterceptor,
): OkHttpClient = OkHttpClient.Builder()
): OkHttpClient = NetworkSecurityUtil.createSecureOkHttpClient()
.addInterceptor(logger)
.addInterceptor(authInterceptor)
.addInterceptor(responseInterceptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main

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

Analytics.logClickedItemEvent(EVENT_VIEW_HOME)
initRemoteConfig()
checkVisitorMode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.runnect.runnect.presentation.composesample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
Expand All @@ -14,6 +15,7 @@ import com.runnect.runnect.presentation.composesample.ui.theme.ComposeSampleThem

class ComposeSampleActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
ComposeSampleTheme {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.runnect.runnect.R
import com.runnect.runnect.data.dto.TimerData
import com.runnect.runnect.util.ForegroundServiceUtil
import com.runnect.runnect.util.IntentUtil
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
Expand Down Expand Up @@ -102,11 +104,12 @@ class TimerService : Service() {
val notificationIntent = Intent(this@TimerService, RunActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)

val pendingIntent = PendingIntent.getActivity(
val pendingIntent = IntentUtil.createSafePendingIntent(
this@TimerService,
0,
notificationIntent,
FLAG_IMMUTABLE
FLAG_IMMUTABLE,
false
)
notificationBuilder.setContentIntent(pendingIntent) // 알림 클릭 시 이동

Expand All @@ -127,7 +130,12 @@ class TimerService : Service() {
notificationBuilder.build()
) // id : 정의해야하는 각 알림의 고유한 int값
val notification = notificationBuilder.build()
startForeground(NOTI_ID, notification)
ForegroundServiceUtil.startForegroundSafely(
this,
NOTI_ID,
notification,
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks
import com.runnect.runnect.application.PreferenceManager
Expand All @@ -18,6 +19,7 @@ import timber.log.Timber
@AndroidEntryPoint
class SchemeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

if (isUserLoggedIn()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import com.runnect.runnect.R
import com.runnect.runnect.presentation.login.LoginActivity
Expand All @@ -14,6 +15,7 @@ class SplashActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
navigateToLoginScreen()
Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/com/runnect/runnect/util/EdgeToEdgeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.runnect.runnect.util

import android.os.Build
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

object EdgeToEdgeUtil {

/**
* Edge-to-Edge를 활성화하고 시스템바 insets을 처리합니다.
* @param activity 대상 Activity
* @param rootView insets을 적용할 루트 뷰
* @param applyPadding 시스템바에 대한 패딩 적용 여부 (기본: true)
*/
fun setupEdgeToEdge(
activity: ComponentActivity,
rootView: View,
applyPadding: Boolean = true
) {
// API 30+ (Android 11+)에서 Edge-to-Edge 활성화
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activity.enableEdgeToEdge()
}

// WindowInsets 처리
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())

if (applyPadding) {
view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
}

insets
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.runnect.runnect.util

import android.app.Service
import android.content.pm.ServiceInfo
import android.os.Build

object ForegroundServiceUtil {


/**
* Service에서 startForeground 호출 시 사용
*/
fun startForegroundSafely(
service: Service,
notificationId: Int,
notification: android.app.Notification,
foregroundServiceType: Int = ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// API 29+ foregroundServiceType 지정
service.startForeground(notificationId, notification, foregroundServiceType)
} else {
service.startForeground(notificationId, notification)
}
} catch (e: Exception) {
e.printStackTrace()
}
}

}
38 changes: 38 additions & 0 deletions app/src/main/java/com/runnect/runnect/util/IntentUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.runnect.runnect.util

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build

object IntentUtil {

/**
* 안전한 PendingIntent를 생성합니다 (API 34+ 대응)
* @param context 컨텍스트
* @param requestCode 요청 코드
* @param intent 대상 인텐트
* @param flags PendingIntent 플래그
* @param isMutable 가변 PendingIntent 여부 (기본: false)
*/
fun createSafePendingIntent(
context: Context,
requestCode: Int,
intent: Intent,
flags: Int = 0,
isMutable: Boolean = false
): PendingIntent {
val finalFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags or if (isMutable) {
PendingIntent.FLAG_MUTABLE
} else {
PendingIntent.FLAG_IMMUTABLE
}
} else {
flags
}

return PendingIntent.getActivity(context, requestCode, intent, finalFlags)
}

}
54 changes: 54 additions & 0 deletions app/src/main/java/com/runnect/runnect/util/NetworkSecurityUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.runnect.runnect.util

import com.runnect.runnect.BuildConfig
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager

object NetworkSecurityUtil {

/**
* TLS 1.2+ 를 강제하는 안전한 OkHttpClient 생성
*/
fun createSecureOkHttpClient(
enableTrustAllCerts: Boolean = false // 개발 환경에서만 true
): OkHttpClient.Builder {
val builder = OkHttpClient.Builder()

// TLS 1.2+ 강제 설정
val connectionSpecs = listOf(
ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.build(),
ConnectionSpec.CLEARTEXT // HTTP 허용 (필요시)
)

builder.connectionSpecs(connectionSpecs)

// 개발 환경에서만 모든 인증서 신뢰 (프로덕션에서는 절대 사용 금지!)
if (enableTrustAllCerts && BuildConfig.DEBUG) {
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())

builder.sslSocketFactory(
sslContext.socketFactory,
trustAllCerts[0] as X509TrustManager
)
builder.hostnameVerifier { _, _ -> true }
}

return builder
}

}
4 changes: 4 additions & 0 deletions app/src/main/res/values/themes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<!-- Status bar color. -->
<item name="android:windowLightStatusBar">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast">false</item>

<!-- Customize your theme here. -->
<item name="android:includeFontPadding">false</item>
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ kotlin.code.style=official
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
android.suppressUnsupportedCompileSdk=35
org.gradle.unsafe.configuration-cache=true
org.gradle.unsafe.configuration-cache-problems=warn
Loading