그러냐

잠금화면 액티비티 띄우기 본문

android

잠금화면 액티비티 띄우기

관절분리 2024. 4. 17. 17:13
반응형

Android에는 잠금화면 액티비티를 개발자가 개발 할 수있다.

 

그런데 스택오버플로우나 여러 블로그에는 똑같은 글만 돌고 도는듯 하고,

기타 오류에 대해서는 해결법을 찾기 어려웠다.

 

잠금화면을 띄우기 위해서는 BroadcastReceiver에서 ACTION_SCREEN_OFF, ACTION _SCREEN_ON 액션을 받아서 

화면에 액티비티를 띄워야 되는데 매니페스트 파일에 receiver를 등록해 정적 동작은 실행되지 않는다.

 

그래서 Service를 만들어 Service에서 멤버 변수로 BroadcastReceiver를 동적으로 생성해서 IntentFilter로 위의 두

액션과 함께 registerReceiver로 등록해야된다.

 

여기서 또 문제는 Service를 Background에서 동작하게 하면 안드로이드 O 버전부터 백그라운드 동작을 죽이기 때문에

일정 시간이 지나면 더 이상 동작하지 않는다. 이걸 해결하기 위해 AlarmManager로 알람을 등록해 서비스를 계속 실행시키는 방법도 있는데 이것은 알람 주기 자체를 안드로이드에서 제한을 하는 것인지 30초이상은 지나야 동작하기 때문에 그전에 서비스가 죽어버리면 그 사이 시간에는 동작하지 않게 된다.

 

다른 해결법은 Service 시작을 startForegroundService로 시작을하고 Service의 onCreate나 onStartCommand 부분에 Notification을 등록하고 startForeground 함수를 호출하는 방법이다. 그런데 여기서 또 문제는 Foreground로 동작하게 해도 운영체제가 이 Service의 Context를 Sleep으로 만들어버리는지 일정시간 지나면 동작하지 않는 문제가 있다. Destroy를 하진 않는걸 보니 Sleep 같은 상태로 만들어버리는 듯하다.

 

이게 여타 블로그나 스택오버플로우에 돌아다니는 기본적은 내용이다.

 

위 문제를 해결하기 위해 Github의 많은 프로젝트들을 검색해서 방법을 알아냈다.

 

잠금화면을 위한 Service를 만들기 위해서는 매니페스트 파일에 permission 설정을 해야된다.

필요한 permission은 android.permission.SYSTEM_ALERT_WINDOW

 

아래부터 소스

소스 언어는 Kotlin

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        checkPermission()
    }

    fun checkPermission() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if(!Settings.canDrawOverlays(this)) {
                val uri = Uri.fromParts("package", packageName, null)
                val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, uri)
                startActivityForResult(intent, 0)
            } else {
                val intent = Intent(applicationContext, LockScreenService::class.java)
                startForegroundService(intent)
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == 0) {
            if(!Settings.canDrawOverlays(this)) {
                Toast.makeText(this, "해라", Toast.LENGTH_LONG).show()
            } else {
                val intent = Intent(applicationContext, LockScreenService::class.java)
                startForegroundService(intent)
            }
        }
    }
}

MainActivity.kt

 

class LockScreenService : Service() {
    private val receiver = object:BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if(intent != null) {
                when(intent.action) {
                    Intent.ACTION_SCREEN_OFF -> {
                        val newIntent = Intent(context, LockScreenActivity::class.java)
                        newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

                        startActivity(newIntent)
                    }
                }
            }
        }
    }

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }

    private final val ALARM_ID = "com.bluewhale.lockscreentest.lockscreen"

    override fun onCreate() {
        super.onCreate()

        val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val channel = NotificationChannel(ALARM_ID, "잠금화면 테스트", NotificationManager.IMPORTANCE_DEFAULT)

        nm.createNotificationChannel(channel)

        val pending = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), PendingIntent.FLAG_CANCEL_CURRENT)

        val notification = Notification.Builder(this, ALARM_ID)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle("잠금화면 서비스")
            .setContentText("잠금화면 서비스 동작중")
            .setContentIntent(pending)
            .build()

        startForeground(1, notification)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val filter = IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_OFF)

        registerReceiver(receiver, filter)

        return Service.START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(receiver)
    }
}

LockScreenService.kt

 

class LockScreenActivity : AppCompatActivity() {

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


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
            setShowWhenLocked(true)
            //setTurnScreenOn(true)
            val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
            keyguardManager.requestDismissKeyguard(this, null)
        } else {
            this.window.addFlags(
                WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or
                    WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
                    WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
        }


        setContentView(R.layout.activity_lock_screen)

        findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                .setAction("Action", null).show()
            finish()
        }
    }
}

LockScreenActivity.kt

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.bluewhale.lockscreentest">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".LockScreenActivity"
            android:label="@string/title_activity_lock_screen"
            android:theme="@style/AppTheme.NoActionBar"
            android:excludeFromRecents="true"
            android:launchMode="singleInstance"
            android:taskAffinity="com.bluewhale.lockscreentest.lockscreen"/>


        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".service.LockScreenService"
            android:enabled="true"
            android:permission="android.permission.SYSTEM_ALERT_WINDOW"
            android:exported="false"/>
    </application>

</manifest>

AndroidManifest.xml

 

매니페스트 파일을 보면 잠금화면 액티비티에 launchMode, excludeFromRecents, taskAffinity를 추가했다.

excludeFromRecents는 최근 화면에 기록되지 않는 것이고, taskAffinity를 따로 설정해주는 이유는 설정하지않으면 다른액티비티와 같은 Task에서 생성되기 떄문에 잠금화면을 종료하면 최근화면에서 이 앱이 사라지게 된다. taskAffinity를 따로 줘서 다른 Task에서 동작하게하면 그런 영향에서 벗어날 수 있다. 최근화면에서 사라지는 현상은 캐시슬라이드의 잠금화면에서도 나타나나는 현상이다.

 

SYSTEM_ALERT_WINDOW 권한을 획득하고 Service 자체 권한에도 SYSTEM_ALERT_WINDOW 권한을 부여한다.

이 권한을 부여하면 안드로이드의 '다른 앱 위에 그리기' 권한이 승인이 되어야 서비스가 실행된다. 그래서 MainActivity에서 권한이 없으면 권한을 획득하기 위한 코드가 있다.

 

LockScreenService에서 receiver의 코드중에 addFlag로 ACTIVITY_NEW_TASK가 있는데 이 Flag를 주지않으면 익셉션이 발생하게 된다. Service에서 Activity를 생성하기 위해서는 이 Flag를 꼭 줘야되도록 되어 있는 듯.

 

LockScreenActivity에서는 다른 여타 블로그에서 알 수 있듯이 ShowWhenLocked와 KeyguardDismiss 를 수행해준다.

 

여기서 핵심은 SYSTEM_ALERT_WINDOW 권한이고 '다른 앱 위에 그리기' 권한을 사용자에게 승인하도록 하는 것이다.

 

혹시 나와 같이 해결법을 찾지 못한 사람들에게 도움이 되길 바란다.

 

 

 

출처 : https://whale-order.tistory.com/10

반응형