"카카오톡 잠금화면처럼 해주세요"
-> 사실 난 잠금화면 기능을 사용하지 않아 어떤 기능인지 몰랐다.
사실 근데 이런것 할때마다 두근두근 거린다.
분명히 내가 못하는거고 미지의 세계를 헤쳐 나가면서 해야하는데 한 단계씩 점점 나아갈때마다 희열을 느낀다.
(우와 진짜 너무 재미있어!) 이런 느낌? 난 체질이 모험가인것같다.
기능 정의
- 사용자는 잠금화면을 설정하거나 제거할 수 있다.
- 잠금화면을 설정하면 앱이 백그라운드에 갔다 x초 후에 돌아오면 잠금화면이 보인다. ( 모든 화면에서 마찬가지 동작 )
- 잠금화면은 Dismiss 할 수 없다.
- (기획상) 서버통신없이 로컬링으로만 작업하고 아이디별로 설정을 저장한다.
개발 과정
- 먼저 Dismiss 할 수 없는 Dialog 만들기
- 백그라운드에서 4초이상 갔다 온 경우 Dialog 띄우기
- 사용자Id와 password를 저장한 auth 테이블을 만든다.
- User 테이블에 넣을수도 있었는데 유저테이블은 서버에서 가져오면 업데이트하고 패스워드는 성격이 달라 분리
- Auth 테이블에 Flow를 걸어 업데이트 됐을때 감지
- 모든 Activity에서 검사해야 한다. ( 어떻게 해야하지?.. )
- 이 프로젝트는 1Activity 가 아니라 다수의 Activity 로 이루어짐
실제 코드
1. Dissmiss 할 수 없는 Dialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also {
it.setOnKeyListener { dialog, keyCode, event ->
keyCode == KeyEvent.KEYCODE_BACK
}
}
}
비밀번호가 맞은 경우에만 Dismiss 할 수 있도록
Password 를 argument로 받아 처리
private const val EXTRA_PASSWORD = "EXTRA_PASSWORD"
fun newInstance(pwd: String): AuthDialogFragment {
return newInstance().apply {
arguments = bundleOf(
EXTRA_PASSWORD to pwd
)
}
}
2. 백그라운드에서 4초이상 갔다 온 경우 Dialog 띄우기
일단 Application의 onResume이 있었으면 좋았겠지만 없기에
액티비티라싸를 콜백을 이용하여 백그라운드에 갔는지 판단할 수 있다.
Application.ActivityLifecycleCallbacks
private const val CONST_BACKGROUND_TIME = 4000L
@JvmStatic
private var _lastActivity: WeakReference<Activity>? = null
@JvmStatic
var currentTime: Long? = null
@JvmStatic
var wasInBackground: Boolean = false
object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {
_lastActivity = WeakReference(activity)
if (currentTime != null
&& ((System.currentTimeMillis() - currentTime!!) > CONST_BACKGROUND_TIME)) {
wasInBackground = true
}
currentTime = null
}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {
wasInBackground = false
}
override fun onActivityStopped(activity: Activity) {
if (_lastActivity?.get() === activity) {
currentTime = System.currentTimeMillis()
_lastActivity = null
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) { }
}
이걸 Util 로 만들어서 사용하려고 한다 ( Application에 해도 된다, 단지 매번 Application에서 접근해서 가져오는게 불편해서 )
cf) count를 +1,-1 하면서 체크하는 구글에서 제공하는 소스도 있긴 하다
이 소스를 이해하기 위해선 액티비티 생명주기를 잘 알아야한다.
단순히 Created -> Started -> Resumed 가 아니라
다른 액티비티로 이동했을때 어떻게 되는지를 알아야한다.
aActivity -> bActivity 로 이동할때는
b가 먼저 Started 하고 a가 Stopped 되기 때문에 (_lastActivity.get() === activity)을 타지 않는다
( b가 _lastActivity에 할당 되고 a가 Stopped 되어 _lastActivity !== aActivity 가 된다.
반면에 bActivity 에서 백그라운드로 이동하면
현재 _lastActivity === bActivity 이므로 Stopped 안의 if문 에서 background로 이동한걸 알 수 있다.
요기에 추가로 4초 조건을 걸고 싶기에 시간 계산까지 해서 wasInBackground를 true 로 만든다.
3. 사용자Id와 password를 저장한 auth 테이블을 만든다.
@Singleton
class FetchAuthUseCase @Inject constructor(
private val repository: AuthRepository,
private val sessionRepository: SessionRepository
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<String?> {
return sessionRepository.fetchUserId()
.filterNotNull()
.flatMapLatest {
repository.fetchPassword(it)
}.map {
it.password
}
}
}
@Query("SELECT * FROM auth WHERE auth.userId = :id")
fun fetch(id: Long): Flow<AuthEntity>
@Entity(tableName = "auth")
internal data class AuthEntity(
@PrimaryKey
@ColumnInfo(name = "userId") val userId: Long,
@ColumnInfo(name = "password") val password: String?
)
유저 아이디가 없을땐 점검하지 않아야하니까 ( 로그인화면에서는 X )
잠금화면을 설정한 경우 -> password = "1234"
해제 한 경우 -> passsword = null
4. 모든 Activity에서 검사
Activity들의 onResume 에서 1번에서 만든 LastActivityUtil의 wasInBackground 변수를 체크
( onStarted 에서 wasInBackground를 변경했고 그 다음 단계인 onResumed에서 검사 )
모든 액티비티들이 abstract class BaseActivity를 상속하고 있음
BaseActivity에서 FetchAuthUseCase를 @Inject 하고 싶지만
abstract class 에서는 @AndroidEntryPoint를 지정 불가
그런데 방법이 있다!!
EntryPoint를 지정해서 넣을 수 있다.
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AuthEntryPoint {
fun fetchAuth(): FetchAuthUseCase
}
EntryPoint 를 만들고
BaseActivity에서
private val authEntryPoint by lazy {
EntryPoints.get(
this.applicationContext,
AuthEntryPoint::class.java
)
}
private val auth by lazy {
authEntryPoint.fetchAuth()
}
UseCase를 Inejct 가능 ( 어떤곳이든 EntryPoint를 지정해서 넣을 수 있구나..)
abstract class BaseActivity{
init{
auth.invoke()
.onEach {
authPasssword = if (!it.isNullOrEmpty()) {
it
} else {
null
}
}.launchIn(lifecycleScope)
}
override fun onResume() {
super.onResume()
if (authPasssword != null && LastActivityUtils.wasInBackground) {
AuthDialogFragment.newInstance(authPasssword!!)
.show(supportFragmentManager, AuthDialogFragment.name)
}
}
companion object {
var authPasssword: String? = null
}
}
(사실 모든 액티비티마다 authPassword를 만드는 건데. 하나의 전역변수로써 컨트롤 하는게 나을것 같다.)
추가 Tip
이런 UI 를 만들때
저 암호가 입력된은 부분을 TextView 로 만들어서 처리했다 ( 그래야 로직이 편해져서 .. )
<TextView
android:id="@+id/tv_pwd"
android:hint="1234"
android:inputType="numberPassword"
android:letterSpacing="1.2"
android:maxLength="4"
android:textColorHint="@color/white"
/>
그리고 textColorHint를 배경과 같게 해서 안보이게 해둔다.
(그 밑의 하단 View 들은 각각 4개 생성해서 하단에 margin을 조절하여 맞춘다.)
그러면 로직은 아래와 같이 단순?해진다.
btCancel.setOnClickListener { dismissSafely() }
btDelete.setOnClickListener { tvPwd.text = tvPwd.text.dropLast(1) }
bt0.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("0") }
bt1.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("1") }
bt2.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("2") }
bt3.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("3") }
bt4.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("4") }
bt5.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("5") }
bt6.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("6") }
bt7.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("7") }
bt8.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("8") }
bt9.setOnClickListener { tvPwd.text = tvPwd.text.toString().plus("9") }
'Android' 카테고리의 다른 글
Gson으로 sealed class 로 맵핑하기 (0) | 2022.10.04 |
---|---|
특정 뷰위에 BottomSheet 올리기 (0) | 2022.09.29 |
소셜 로그인 모듈화 과정 ( 추상화 및 액티비티 의존성 제거 ) (1) | 2022.09.29 |
transition animation 적용 (0) | 2022.08.14 |
Room With Coroutine Flow (0) | 2022.07.30 |