본문 바로가기
⛏️ | 개발 기록/🪐 | Cosmic Detox

[Android] runnable, handler를 이용해 현재 열려 있는 앱 감지

by immgga 2024. 9. 5.

출처: unsplash.com

 

부트캠프 최종 팀 프로젝트 기록 6

 

 

서론

이번에는 내가 맡은 기능의 마지막 기능인 허용 앱에 들어갔을 때, 다른 앱을 실행하게 되면 그걸 감지해서 overlay를 띄워주는 기능을 구현할 것이다.

다른 앱을 실행하고 나면, 기존의 화면은 background 상태에서 계속 돌아간다는 점을 이용해서 별도의 Thread를 만들어서 현재 열려 있는 앱의 package name을 불러오는 작업을 진행할 것이다.

 

 

허용 앱을 실행했을 때, 다른 앱 실행 감지 기능 구현하기

다른 앱 실행 감지를 위해서는 usageStatsManager를 이용해서 특정 기간 내의 앱 실행 기록을 구할 수 있다.

실행 기록을 가져오는 함수를 만들고, 그 함수를 별도의 Thread에서 반복시켜서 실시간으로 현재 실행되어 있는 앱의 package name을 불러올 것이다.

그리고 현재 package가 자기 자신(Cosmic Detox)과 허용 앱, 시스템 ui, app launcher가 아닌 다른 package에 들어가게 되면 overlay를 띄워주는 작업을 진행할 것이다.

 

 

다른 앱 실행 감지

다른 앱 실행을 감지하기 위해서는 UsageStatsManager에서 queryEvents를 사용해 주면 된다.

private fun getCurrentOpenedAppPackageName(context: Context): String {
    val currentTime = System.currentTimeMillis()
    val startTime = currentTime - 1500
    val usageStatsManager: UsageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager

    val usageEvents = usageStatsManager.queryEvents(startTime, currentTime)
    var lastEvent: String = context.packageName

    while (usageEvents.hasNextEvent()) {
        val event = UsageEvents.Event()
        usageEvents.getNextEvent(event)

        if (isForeGround(event)) {
            lastEvent = event.packageName
        }
    }

    return lastEvent
}

usageStatsManager를 만들어 주고, queryEvent로 현재 시간과 현재 시간에서 1.5초 전에 실행된 앱들의 Event들을 불러온다.

1.5초인 이유는 나중에 Thread에서 1.5초의 텀을 두고 반복시킬 것이기 때문이다.

 

queryEvents에서 불러온 event가 foreGround에 있는지 확인하기 위해서 isForeground 함수를 조건으로 실행한다.

private fun isForeGround(event: UsageEvents.Event): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        event.eventType == UsageEvents.Event.ACTIVITY_RESUMED
    } else {
        event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND
    }
}

원래 MOVE_TO_FOREGROUND를 쓰면 되지만 Deprecated 되었기 때문에 조건을 추가해 준다.

위 로직으로 현재 시간에서 1.5초 전까지 시간에서 실행한 앱들의 정보를 불러올 수 있다.

 

 

Runnable, Thread로 실시간 로직 구성

이제 Runnable을 만들어서 Thread에 실행시킬 준비를 해주겠다.

fun initObserveAppOpenRunnable(
    context: Context,
    currentOpenAppPackage: String,
    showOverlay: () -> Unit
): Runnable {
    return Runnable {
        while (_running.value) {
            val currentPackageName = getCurrentOpenedAppPackageName(context)

            if (currentPackageName != context.packageName && currentPackageName != currentOpenAppPackage &&
                currentPackageName != "com.sec.android.app.launcher" && currentPackageName != "com.android.systemui") {

                Handler(Looper.getMainLooper()).postDelayed({
                    showOverlay()
                }, 0)
            }

            Thread.sleep(1500)
        }
    }
}

이 코드에서는 Runnable을 return 하도록 한다. 위에서 구현한 현재 package name을 가져오는 로직을 구현하고,

불러온 package name이 위 4개가 아니라면 showOverlay()를 실행한다.

_running.value를 Runnable을 시작하고 끝내기 위한 트리거이다.

Thread.sleep(1500)을 사용해 무분별하게 데이터를 불러오지 않도록 해준다(텀이 더 빨라도 되지만, 1.5초가 제일 적당해 보였다).

 

Runnable의 트리거를 설정하는 함수들도 생성해 주었다.

fun startObserveAppOpenRunnable() {
    _running.value = true
}

fun stopObserveAppOpenRunnable() {
    _running.value = false
}

 

 

Fragment에 적용

내 로직은 허용 앱을 클릭하게 되면 허용 앱으로 넘어가면서 Runnable을 실행해야 한다.

private val adapter by lazy {
    AllowedAppAdapter(requireContext()) { packageId, limitedTime ->
        isChecked = true
        val intent = context?.packageManager?.getLaunchIntentForPackage(packageId)
        context?.startActivity(intent)

        initCountDownTimer(limitedTime)
        allowedAppViewModel.setSelectedAllowedAppPackage(packageId)
        allowedAppViewModel.startObserveAppOpenRunnable()
        val runnable = allowedAppViewModel.initObserveAppOpenRunnable(requireContext(), packageId) {
            if (rootView == null) {
                showOverlay()
                allowedAppViewModel.stopObserveAppOpenRunnable()
            }
        }
        val thread = Thread(runnable)
        thread.start()
    }
}

Adapter의 item(허용 앱 아이템)을 클릭했을 때, Thread를 생성해서 runnable에 연결해 주고, thread를 start 한다.

runnable 안에는 showOverlay를 lambda로 전개해서 overlay를 show 하고, runnable을 정지시킨다.

 

 

구현 중 발생한 문제 상황

이 로직도 역시 Fragment에서 다이렉트로 구현할 수 있는 코드였는데, 안 되는 줄 알고 Service에서 구현하려다가 시간을 많이 소비했다.

Service로 구현했을 때는, 앱 제한 시간이 끝났을 때 자동으로 우리 앱으로 복귀하는 기능, 허용 앱을 보고 나서 우리 앱으로 돌아오는 기능은 정상 작동 했는데

허용 앱 감지 부분에서 계속 오류가 발생했다.

Service를 start 시켜 작동시켜 놓으면, 계속 돌아가다가 1분 정도 지나니까 자동으로 Service가 종료되는 문제가 있었다.

아직까지도 왜 그런지는 이유를 모르겠다.

지난 포스팅의 CountDownTimer 때처럼 "혹시 Fragment에서 구현이 되나?" 싶어서 구현해 봤는데 된다..

 

 

정리

Service 삽질을 2번 정도 하면서 Service를 사용하는 법은 완벽히 숙지한 것 같다.

하지만 아직까지는 제대로 사용해 본 적이 없어서 나중에 기회가 있으면 적용해 보겠다.

728x90