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
11 changes: 11 additions & 0 deletions app/src/main/java/kr/co/call/callfromai/AppScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ import kr.co.call.api.TermNavKey
import kr.co.call.callfromai.ui.MainBottomBar
import kr.co.call.callfromai.ui.MainTab
import kr.co.call.callfromai.util.toMainTab
import kr.co.call.callfromai.sideeffect.AppSideEffect
import kr.co.call.designsystem.component.LocalBottomBarPadding
import kr.co.call.impl.entry.chattingEntry
import kr.co.call.impl.entry.callEntry
import kr.co.call.impl.entry.homeEntry
import kr.co.call.impl.entry.loginEntry
import kr.co.call.impl.entry.myPageEntry
import kr.co.call.impl.entry.onboardingEntry
import org.orbitmvi.orbit.compose.collectSideEffect

/**
* 애플리케이션 화면 내비게이션의 메인 진입점입니다.
Expand All @@ -80,6 +82,14 @@ fun AppScreen(
val appNavigator = remember(backStack) { AppNavigator(backStack) }
val currentKey = backStack.lastOrNull()

viewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
AppSideEffect.NavigateToLogin -> {
appNavigator.replaceAll(LoginNavKey)
}
}
}

// TODO: 각자 구현하면서 피그마 보고 추가하기!!
val showBottomBar = when (currentKey) {
is HomeNavKey,
Expand Down Expand Up @@ -229,6 +239,7 @@ fun AppScreen(
}
)
myPageEntry(
navigateToLogin = {appNavigator.replaceAll(LoginNavKey)},
navigateToFaq = { appNavigator.navigate(FaqNavKey) },
navigateToTerms = { appNavigator.navigate(TermNavKey) },
navigateToCharacterManagement = { appNavigator.navigate(CharacterManagementNavKey) },
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/kr/co/call/callfromai/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kr.co.call.callfromai.intent.AppIntent
import kr.co.call.callfromai.sideeffect.AppSideEffect
import kr.co.call.callfromai.state.AppState
import kr.co.call.datastore.AuthSessionManager
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
import javax.inject.Inject

@HiltViewModel
class AppViewModel @Inject constructor(

private val authSessionManager: AuthSessionManager,
) : ViewModel(), ContainerHost<AppState, AppSideEffect> {

override val container: Container<AppState, AppSideEffect> = container(
initialState = AppState()
)

init {
intent {
authSessionManager.sessionExpired.collect {
postSideEffect(AppSideEffect.NavigateToLogin)
}
}
}

fun handleIntent(intent: AppIntent) {
when (intent) {
else -> Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ package kr.co.call.callfromai.sideeffect
* ```
*/
sealed interface AppSideEffect {

}
data object NavigateToLogin : AppSideEffect
}
4 changes: 3 additions & 1 deletion app/src/main/res/xml/backup_rules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
<!-- 리프레시 토큰 등 인증 정보가 백업에 포함되면 앱 삭제 후 재설치해도 자동 로그인되는 버그 발생 -->
<exclude domain="file" path="datastore/call_from_ai_token_prefs.preferences_pb"/>
</full-backup-content>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4 changes: 3 additions & 1 deletion app/src/main/res/xml/data_extraction_rules.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
<include .../>
<exclude .../>
-->
<!-- 리프레시 토큰 등 인증 정보가 클라우드 백업에 포함되면 앱 삭제 후 재설치해도 자동 로그인되는 버그 발생 -->
<exclude domain="file" path="datastore/call_from_ai_token_prefs.preferences_pb"/>
Comment on lines +12 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== dataExtractionRules 참조 확인 =="
rg -n -C 3 'dataExtractionRules|fullBackupContent' -g 'AndroidManifest.xml' .

echo "== 현재 데이터 추출 규칙 확인 =="
cat -n app/src/main/res/xml/data_extraction_rules.xml

Repository: call-from-ai/CallFromAi_Android

Length of output: 1729


🌐 Web query:

Android 12 device transfer data restoration excluded backup data documentation exclude device transfer

💡 Result:

For devices running Android 12 (API level 31) and higher, the android:allowBackup and android:fullBackupContent attributes are deprecated for controlling device-to-device (D2D) transfers and now apply only to cloud-based backups [1]. To control data exclusion for both cloud backups and D2D transfers on Android 12+, you must use the android:dataExtractionRules attribute in your AndroidManifest.xml [2][3][4]. Configuration Procedure 1. Update AndroidManifest.xml: Add the android:dataExtractionRules attribute pointing to an XML resource file (e.g., @xml/data_extraction_rules) [3][5]. <application... android:dataExtractionRules="@xml/data_extraction_rules"> 2. Create the Rules File: Define your inclusion and exclusion rules within the res/xml/data_extraction_rules.xml file [3][5]. This file allows you to define rules separately for cloud-backup and device-transfer [3][4]. Example Configuration: Key Considerations * D2D Transfers: If the android:dataExtractionRules attribute is not specified, the system defaults to transferring all application data during a D2D migration [2]. * Backward Compatibility: If your app supports Android 11 (API level 30) or lower, you must still maintain android:fullBackupContent and its associated backup rules file to ensure consistent behavior across all supported Android versions [2][1][5]. * Granular Control: You can use the include and exclude tags within both cloud-backup and device-transfer blocks to specify which domains (e.g., file, database, sharedpref, external, root) and specific file paths to include or exclude [3][4]. Note that if you use an include tag, the system will only back up or transfer the specified files, ignoring others by default [6]. For further guidance, consult the official Android documentation on Backup Best Practices and Auto Backup.

Citations:


🌐 Web query:

Android default device transfer includes all app data unless data extraction rules exclude

💡 Result:

The statement is generally correct regarding how Android handles app data during migration. By default, the Android system includes application data in both cloud backups and device-to-device (D2D) transfers unless the application developer explicitly excludes it using configuration rules [1][2]. Key details regarding this process include: 1. Automatic Inclusion: For most apps targeting Android 6.0 (API level 23) or higher, Auto Backup is enabled by default [2]. If no specific backup or data extraction rules are defined, the system will attempt to transfer the app's data during a device migration [1]. 2. Configuration Rules: Developers can control this behavior using XML configuration files. - On Android 12 (API level 31) and higher, developers use the android:dataExtractionRules attribute in the AndroidManifest.xml to point to a configuration file that defines and rules [3][4]. - If the tag is not configured or does not include an explicit exclusion, the default behavior remains to transfer the application's data [1]. - Developers can use these rules to include or exclude specific domains (such as file, database, sharedpref, external, or root) and paths [3][5]. 3. Limitations: Not all app data is guaranteed to transfer [6][7]. The success of a transfer depends on: - The application developer's implementation: If an app does not use the standard Android backup framework or has explicitly excluded its data, that data will not be transferred [6][2]. - System limitations: Certain types of data, such as app data for apps not from the Google Play Store or data from apps that do not utilize Android backup, may not be included [6]. - Version compatibility: Rules for data extraction were refined in Android 12, requiring developers to ensure their backup configurations are compatible across different Android versions [1][4]. In summary, while the system defaults to including app data, developers retain the authority to exclude specific data or prevent it from being backed up/transferred entirely through defined extraction rules [1][4]. Users should be aware that the actual data transferred can vary by app [6][7].

Citations:


🌐 Web query:

Android BackupDataTransport DeviceShadowFileAgent device transfer fullBackupContent all app data without data extraction rules

💡 Result:

The terms you mentioned relate to different layers and concepts within the Android backup and data transfer infrastructure: BackupDataTransport is not a standard public API class but rather refers to the underlying transport mechanism (often implementing the IBackupTransport interface) that handles the movement of backup data to storage or between devices [1][2][3]. DeviceShadowFileAgent is not a documented Android system class or standard API component. It is possible this is an internal identifier or a misunderstanding of how the system "shadows" or handles file-based backups during data migration. FullBackupContent refers to the android:fullBackupContent attribute in an app's AndroidManifest.xml [4][5]. This attribute allows developers to point to an XML resource file that defines rules for including or excluding specific files and directories during Auto Backup and device-to-device transfers [6][4][7]. When an app does not define data extraction rules (or an explicit fullBackupContent/dataExtractionRules configuration), the Android system's default behavior is to include almost all data within the application's local directory (such as /data/data/<package_name>/) in the backup set, provided that android:allowBackup is set to true (or is left as the default true) [4][5]. In the context of device-to-device transfers, the system uses these same rules. If no specific exclusions are defined, the backup agent—which is responsible for the interaction between the application and the backup system—will process all accessible data files unless otherwise restricted by the system's own security policies [8][6]. Because backup and restoration involve an app's private files, failure to explicitly define these rules can lead to the accidental inclusion of sensitive information (like tokens, cache, or device-specific identifiers) in transfers to other devices [4][7].

Citations:


🌐 Web query:

Android frameworks base BackupManagerService shouldIncludeDeviceTransfer device shadow file device transfer not including all app data

💡 Result:

In the Android backup framework, there is no public or standard internal method named "shouldIncludeDeviceTransfer" within the BackupManagerService or related classes. The mechanism for determining what data is included in a device-to-device (D2D) transfer or cloud backup is primarily controlled through the application's backup configuration, specifically the backup_rules.xml file [1][2]. If an application is not including all expected app data during a device transfer, it is typically due to one of the following factors rather than a framework-level "shadow file" issue: 1. Configuration Rules (backup_rules.xml): Developers define which data to include or exclude for cloud backups and device-to-device transfers using the and tags in their configuration XML [1]. If data is missing, it is often because it falls under an excluded domain or path, or because the specific rules were not correctly configured to include the desired data [1][2]. 2. Transport Capabilities: Device-to-device transfer uses a specific transport mechanism (often the D2dTransport) [3][4]. Not all app data is guaranteed to be transferred if it does not meet the requirements of this transport or if the app has opted out of certain types of backups [1][2]. 3. Security and Encryption: Android may exclude data from backups—especially during automated processes—if the data is marked as sensitive or if the device does not meet specific security requirements (such as having a screen lock set for client-side encryption) [1][2]. 4. Backup Agent Implementation: Applications can override default behavior by implementing a BackupAgent [5]. If the agent is not correctly configured to handle full-backup or key-value data, or if it explicitly excludes files during the backup process, the data will not be available for transfer [6][5]. To debug issues where app data is missing after a transfer, developers should verify their backup_rules.xml to ensure proper inclusion of required domains (such as device_file, device_database, or root) [1] and confirm that the BackupAgent is not filtering out the files during the traversal of the backup file tree [6]. You can test this behavior by using the bmgr tool to simulate backups and inspect what data is being handled by the active transport [3][4].

Citations:


device-transfer에서도 토큰 DataStore를 제외하세요.

android:dataExtractionRules="@xml/data_extraction_rules"은 현재 cloud-backup만 적용합니다. device-transfer 규칙은 비활성 상태라 Android 12 이상 기기 간 전송 경로에서 토큰 파일이 포함될 수 있습니다. device-transfer를 활성화하고 datastore/call_from_ai_token_prefs.preferences_pb도 제외하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/xml/data_extraction_rules.xml` around lines 12 - 13, Update
the data extraction rules to enable the device-transfer configuration and
exclude datastore/call_from_ai_token_prefs.preferences_pb there as well as in
the existing cloud-backup rules, preserving the token exclusion for both backup
paths.

</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
</data-extraction-rules>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import kr.co.call.data.repositoryImpl.AICharacterRepositoryImpl
import kr.co.call.data.repositoryImpl.CallRecordRepositoryImpl
import kr.co.call.data.repositoryImpl.FaqRepositoryImpl
import kr.co.call.data.repositoryImpl.ChatRepositoryImpl
import kr.co.call.data.repositoryImpl.HomeMockRepository
import kr.co.call.data.repositoryImpl.OnboardingRepositoryImpl
import kr.co.call.domain.repository.OnboardingRepository
import kr.co.call.domain.repository.AICharacterRepository
import kr.co.call.domain.repository.CallRecordRepository
import kr.co.call.domain.repository.ChatRepository
Expand Down Expand Up @@ -95,4 +98,10 @@ abstract class RepositoryModule {
abstract fun bindCallSessionRepository(
impl: AndroidCallSessionRepository,
): CallSessionRepository

@Binds
@Singleton
abstract fun bindOnboardingRepository(
impl: OnboardingRepositoryImpl,
): OnboardingRepository
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package kr.co.call.data.repositoryImpl

import javax.inject.Inject
import kr.co.call.data.util.toAppResult
import kr.co.call.data.util.safeApiResult
import kr.co.call.datastore.TokenDataStore
import kr.co.call.domain.model.login.LoginToken
import kr.co.call.domain.repository.LoginRepository
import kr.co.call.network.api.LoginApi
import kr.co.call.network.dto.login.LoginRequestDto
import kr.co.call.network.util.ErrorResponseParser
import kr.co.call.network.util.safeApiCall

/**
* 카카오 로그인 서버 통신을 실제로 수행하는 Repository 구현체입니다.
Expand All @@ -27,20 +26,19 @@ class LoginRepositoryImpl @Inject constructor(
override suspend fun loginWithKakao(
kakaoAccessToken: String,
): Result<LoginToken> =
runCatching {
val result = safeApiCall(errorResponseParser) {
loginApi.login(
request = LoginRequestDto(
kakaoAccessToken = kakaoAccessToken,
),
)
}
safeApiResult(errorResponseParser) {
loginApi.login(
request = LoginRequestDto(
kakaoAccessToken = kakaoAccessToken,
),
)
}.mapCatching { result ->
require(
result.accessToken.isNotBlank() && result.refreshToken.isNotBlank(),
) {
"로그인 응답 토큰 값이 비어 있습니다"
}
tokenDataStore.saveTokens(
tokenDataStore.setTokens(
accessToken = result.accessToken,
refreshToken = result.refreshToken,
)
Expand All @@ -51,5 +49,5 @@ class LoginRepositoryImpl @Inject constructor(
needsOnboarding = result.needsOnboarding,
needsTermsAgreement = result.needsTermsAgreement,
)
}.toAppResult()
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,53 @@
package kr.co.call.data.repositoryImpl

import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kr.co.call.data.util.safeApiResultUnit
import kr.co.call.datastore.TokenDataStore
import kr.co.call.domain.model.mypage.MyPageProfile
import kr.co.call.domain.repository.MyPageRepository
import kr.co.call.network.api.MyPageApi
import kr.co.call.network.util.ErrorResponseParser
import timber.log.Timber
import javax.inject.Inject

// TODO: API 연동 전 임시 구현체
class MyPageRepositoryImpl @Inject constructor() : MyPageRepository {
class MyPageRepositoryImpl @Inject constructor(
private val myPageApi: MyPageApi,
private val tokenDataStore: TokenDataStore,
private val errorResponseParser: ErrorResponseParser
) : MyPageRepository {

override suspend fun getMyProfile(): Result<MyPageProfile> {
delay(500)
return runCatching {
return Result.success(
MyPageProfile(
profileImageUrl = "",
nickname = "김수현",
tier = "Basic",
remainingTicketCount = 18,
appVersion = "1.0.0",
)
}
),
)
}

override suspend fun logout() {
delay(300)
}
override suspend fun logout(): Result<Unit> =
safeApiResultUnit(errorResponseParser) {
myPageApi.logout()
}.mapCatching {
tokenDataStore.clearTokens()
}

override suspend fun deleteAccount() {
delay(300)
}
}
override suspend fun deleteAccount(): Result<Unit> =
safeApiResultUnit(errorResponseParser) {
myPageApi.deleteAccount()
}.onSuccess {
try {
tokenDataStore.clearTokens()
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
Timber.w(error, "회원 탈퇴 후 토큰 삭제 실패")
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package kr.co.call.data.repositoryImpl

import javax.inject.Inject
import kr.co.call.data.util.safeApiResultUnit
import kr.co.call.domain.model.onboarding.CharacterOnboardingInput
import kr.co.call.domain.model.onboarding.MemberOnboardingInput
import kr.co.call.domain.repository.OnboardingRepository
import kr.co.call.network.api.AICharacterApi
import kr.co.call.network.api.MyPageApi
import kr.co.call.network.dto.onboarding.CharacterTraitRequestDto
import kr.co.call.network.dto.onboarding.CreateCharacterRequestDto
import kr.co.call.network.dto.onboarding.UpdateMemberRequestDto
import kr.co.call.network.util.ErrorResponseParser

class OnboardingRepositoryImpl @Inject constructor(
private val myPageApi: MyPageApi,
private val aiCharacterApi: AICharacterApi,
private val errorResponseParser: ErrorResponseParser,
) : OnboardingRepository {

override suspend fun submitMemberOnboarding(
submission: MemberOnboardingInput,
): Result<Unit> =
safeApiResultUnit(errorResponseParser) {
myPageApi.updateMember(
request = UpdateMemberRequestDto(
lastName = submission.lastName,
firstName = submission.firstName,
imageUrl = submission.imageUrl,
gender = submission.gender,
birth = submission.birth,
mbti = submission.mbti,
job = submission.job,
),
)
}

override suspend fun submitCharacterOnboarding(
submission: CharacterOnboardingInput,
): Result<Unit> =
safeApiResultUnit(errorResponseParser) {
aiCharacterApi.createCharacter(
request = CreateCharacterRequestDto(
lastName = submission.lastName,
firstName = submission.firstName,
gender = submission.gender,
age = submission.age,
job = submission.job,
imageUrl = submission.imageUrl,
spiceLevel = submission.spiceLevel,
preferTime = submission.preferTime,
mbti = submission.mbti,
speechStyle = submission.speechStyle,
relationshipStage =
submission.relationshipStage,
traits = submission.traits.map { trait ->
CharacterTraitRequestDto(
trait = trait.trait,
priority = trait.priority,
)
},
),
)
}
}
Loading