From 0d15a09ba99aee61840d282c0f833e0f8792e8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nami=2Eand=28=EB=82=98=EB=AF=B8=29?= Date: Tue, 26 Aug 2025 00:09:29 +0900 Subject: [PATCH] =?UTF-8?q?target=2034,35=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 23 ++++++-- .../runnect/binding/BindingActivity.kt | 4 ++ .../com/runnect/runnect/di/RetrofitModule.kt | 5 +- .../runnect/presentation/MainActivity.kt | 1 + .../composesample/ComposeSampleActivity.kt | 2 + .../runnect/presentation/run/TimerService.kt | 14 +++-- .../presentation/scheme/SchemeActivity.kt | 2 + .../presentation/splash/SplashActivity.kt | 2 + .../runnect/runnect/util/EdgeToEdgeUtil.kt | 40 ++++++++++++++ .../runnect/util/ForegroundServiceUtil.kt | 31 +++++++++++ .../com/runnect/runnect/util/IntentUtil.kt | 38 +++++++++++++ .../runnect/util/NetworkSecurityUtil.kt | 54 +++++++++++++++++++ app/src/main/res/values/themes.xml | 4 ++ gradle.properties | 1 + 15 files changed, 214 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/runnect/runnect/util/EdgeToEdgeUtil.kt create mode 100644 app/src/main/java/com/runnect/runnect/util/ForegroundServiceUtil.kt create mode 100644 app/src/main/java/com/runnect/runnect/util/IntentUtil.kt create mode 100644 app/src/main/java/com/runnect/runnect/util/NetworkSecurityUtil.kt diff --git a/app/build.gradle b/app/build.gradle index 575c8c1b6..93c199fad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9dab9288b..9d2cd19e4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,27 @@ - - - + + + + + + + + + + + + + + + + @@ -32,7 +46,8 @@ + android:exported="false" + android:foregroundServiceType="location" /> (@LayoutRes private val layoutResId: Int) : AppCompatActivity() { @@ -13,5 +14,8 @@ abstract class BindingActivity(@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) } } \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt b/app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt index f8704f069..f2042fcb8 100644 --- a/app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt +++ b/app/src/main/java/com/runnect/runnect/di/RetrofitModule.kt @@ -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 @@ -66,7 +67,7 @@ object RetrofitModule { fun provideOkHttpClient( logger: HttpLoggingInterceptor, @Auth authInterceptor: Interceptor - ): OkHttpClient = OkHttpClient.Builder() + ): OkHttpClient = NetworkSecurityUtil.createSecureOkHttpClient() .addInterceptor(logger) .addInterceptor(authInterceptor) .build() @@ -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) diff --git a/app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt index 56f89b2f1..df7300ebb 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/MainActivity.kt @@ -34,6 +34,7 @@ class MainActivity : BindingActivity(R.layout.activity_main override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Analytics.logClickedItemEvent(EVENT_VIEW_HOME) initRemoteConfig() checkVisitorMode() diff --git a/app/src/main/java/com/runnect/runnect/presentation/composesample/ComposeSampleActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/composesample/ComposeSampleActivity.kt index 3035cbd39..c8f830a27 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/composesample/ComposeSampleActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/composesample/ComposeSampleActivity.kt @@ -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 @@ -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 { diff --git a/app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt b/app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt index 75224e96c..e8037b74c 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/run/TimerService.kt @@ -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 @@ -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) // 알림 클릭 시 이동 @@ -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 { diff --git a/app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt index 8a8caaa59..40270d027 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/scheme/SchemeActivity.kt @@ -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 @@ -18,6 +19,7 @@ import timber.log.Timber @AndroidEntryPoint class SchemeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) if (isUserLoggedIn()) { diff --git a/app/src/main/java/com/runnect/runnect/presentation/splash/SplashActivity.kt b/app/src/main/java/com/runnect/runnect/presentation/splash/SplashActivity.kt index 91cf892a0..c0192cdc2 100644 --- a/app/src/main/java/com/runnect/runnect/presentation/splash/SplashActivity.kt +++ b/app/src/main/java/com/runnect/runnect/presentation/splash/SplashActivity.kt @@ -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 @@ -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() diff --git a/app/src/main/java/com/runnect/runnect/util/EdgeToEdgeUtil.kt b/app/src/main/java/com/runnect/runnect/util/EdgeToEdgeUtil.kt new file mode 100644 index 000000000..04b835166 --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/util/EdgeToEdgeUtil.kt @@ -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 + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/util/ForegroundServiceUtil.kt b/app/src/main/java/com/runnect/runnect/util/ForegroundServiceUtil.kt new file mode 100644 index 000000000..1dc5db737 --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/util/ForegroundServiceUtil.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/util/IntentUtil.kt b/app/src/main/java/com/runnect/runnect/util/IntentUtil.kt new file mode 100644 index 000000000..9d12434b3 --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/util/IntentUtil.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/runnect/runnect/util/NetworkSecurityUtil.kt b/app/src/main/java/com/runnect/runnect/util/NetworkSecurityUtil.kt new file mode 100644 index 000000000..e1e20f531 --- /dev/null +++ b/app/src/main/java/com/runnect/runnect/util/NetworkSecurityUtil.kt @@ -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(object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) {} + override fun checkServerTrusted(chain: Array, authType: String) {} + override fun getAcceptedIssuers(): Array = arrayOf() + }) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustAllCerts, SecureRandom()) + + builder.sslSocketFactory( + sslContext.socketFactory, + trustAllCerts[0] as X509TrustManager + ) + builder.hostnameVerifier { _, _ -> true } + } + + return builder + } + +} \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0c5283a14..d7f9d7df3 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -14,6 +14,10 @@ true @android:color/transparent + @android:color/transparent + shortEdges + false + false false diff --git a/gradle.properties b/gradle.properties index f93b6c72f..d3b58cae7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 \ No newline at end of file