현재 잠금 화면 앱을 개발중에 있습니다. 잠금화면을 구현하기 위해서 필요한 요소들이 어떤게 있는지 얕게 알아보도록하겠습니다.
잠금화면을 만들기 위한 개요는 다음과 같습니다.
Launcher Activity부터 하나씩 알아보도록하겠습니다.
Launcher Activity
Launcher Activity에서는 잠금화면 활성화나, 비밀번호 로직, Permission 체크 등의 기능이 들어갑니다.
잠금화면을 사용할 것 인지 아닌지를 여기서 on off 할 수 있습니다.
또한 Permission check는 잠금화면이 필요한 권한들을 확인합니다.
잠금 화면이 필수적으로 가져야 할 권한은 알림 접근 허용 과 다른 앱 위의 표시 권한입니다.
알림 접근 허용은 Android의 Notification에 접근해서 가져올 수 있는 권한을 말합니다.
이 권한이 있다면, 현재 휴대폰으로 오는 알림들에 대해 접근할 수 있습니다.
다른 앱 위의 표시 권한은 잠금화면의 특성한 최상단에 View가 보여져야합니다. 이 특성을 만족시키기 위한 권한입니다.
잠금화면이 활성화가 된다면, Notification Listener Service를 포그라운드 서비스로 실행합니다.
Notification Listener Service
Notification Listener Service는 알림이 오게 되면 callBack을 만들 수 있는 Service입니다.
Service를 상속하고 있기 때문에 서비스의 기능도 가지고 있습니다.
잠금화면에서 알림들을 표시해야하는 기능이 필요하기 때문에 NotificationListenerService를 실행시킵니다.
override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
}
override fun onNotificationRemoved(sbn: StatusBarNotification?) {
super.onNotificationRemoved(sbn)
}
위의 두개의 메소드를 통해서 알림이 수신되거나, 제거되었을 때 제어할 수 있습니다.
ForegroundService의 특징으로는 앱을 종료해도 계속 실행되고 있다는 점입니다. 서비스가 종료되거나 포그라운드에서 제거되지 않는다면 계속 살아있습니다. 이 부분을 이용하여 계속 Notification을 수신할 수 있습니다.
또한 잠금 화면 앱은 사용자가 직접 이 앱을 비활성화 하지 않는 이상, x어떠한 이유로도 서비스가 중지되도 다시 실행할 수 있어야하기 때문에,
서비스가 종료되어도 다시 이 서비스를 다시 시작할 수 있도록, NotificationListenerService의 onStartCommand에서 START_REDELIVER_INTENT Flag를 지정해야합니다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
{...}
return START_REDELIVER_INTENT
}
잠금화면은 휴대폰의 화면이 꺼지고 다시 켜졌을 때 잠금 화면을 보여줍니다. 화면의 상태를 수신 받을 수 있는 BroadCast Receiver를 여기서 등록시켜줍니다.
BroadCast Receiver에서는 화면이 꺼졌을 때, 그리고 앱이 부팅됐을 때의 intent를 수신할 것입니다. 따라서 서비스에서 리시버를 등록해주겠습니다.
// In NotificationListenerService onCreate
val intentFilter = IntentFilter().apply{
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_BOOT_COMPLETED)
}
context.registerReceiver(receiver, intentFilter)
API 26이상부터는 백그라운드 실행 제한으로 인하여 몇가지를 제외하고는 Manifest.xml에 암시적으로 리시버를 등록해줄 수 없습니다.
따라서 위의 코드와 같이 동적으로 등록해주어야합니다.
Broadcast Receiver
브로드캐스트 리시버의 특징은 등록한 intent를 수신할 수 있다는 것입니다. 우리는 화면이 꺼졌을 때와 부팅이 됐을 때의 intent를 수신받고 이를 callBack을 통해 NotificationListenerService에 위의 intent를 수신했음을 알리는 역할을 합니다.
// In Receiver
class ScreenEventReceiver(
private val context: Context,
private val onScreenEventListener: OnScreenEventListener
) : BroadcastReceiver() {
}
interface OnScreenEventListener {
fun openLockScreenByIntent()
}
위와 같이 인터페이스를 정의하고 callBack을 생성자에 등록해줍니다.
// Notification Listener Service
private val screenEventReceiver by lazy {
ScreenEventReceiver( // Broadcast Receiver
context = this,
onScreenEventListener = object : OnScreenEventListener {
override fun openLockScreenByIntent() {
addLockScreen() // 잠금 화면을 띄우자
}
}
)
}
이제 인텐트를 수신하면, 잠금 화면을 띄울 차례입니다!
Service
Service는 getSystemService 메소드를 통해, System-Level의 서비스들에 접근할 수 있습니다.
저희가 접근할 서비스는 WindowService입니다. getSystemService(WINDOW_SERVICE)를 호출하면 WindowManager 객체를 얻을 수 있습니다.
잠금화면은 View의 속성을 Overlay를 적용시켜서 다른 앱이 실행되도, 최상위에서 보여질 수 있도록 구현을 해야합니다.
Android에서 View의 Overlay를 구현하기 위해서는 WindowManager를 통해서 View를 레이아웃 속성과 함께 등록할 수 있습니다.
위의 화면에 서로 다른 색깔의 테두리를 가지고 있는 영역이 모두 Window에 해당합니다.
이러한 Window는 WindowManager가 관리하고 있습니다.
잠금 화면의 특성상 상태바가 숨겨지도록 하거나, 하단 네비게이션 바가 보여지지 않도록 해야할 경우가 생깁니다.
그럴때 WindowManager를 사용해서 잠금화면의 특성상 상단 StatusBar 나 하단 NavigationBar의 속성을 수정하거나, Overlay 기능을 사용하기 위해서 WindowManager를 사용합니다.
Overlay 기능을 사용하기 위해서는 다음과 같은 권한이 필요합니다.
위의 Launcher Activity에서 이 권한을 체크합니다.
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
SYSTEM_ALERT_WINDOW 권한은 앱이 다른 모든 앱 위에 표시될 수 있는 권한을 허용합니다.
앱이 API 23이상을 대상으로 하는 경우 이 권한은 사용자가 직접 설정에서 부여를 해야합니다.
if(Settings.canDrawOaylays(context)){ /*허용함*/} else { /* 허용안함 */}
사용자가 권한을 허용했는지는 위의 코드를 호출하여 현재 권한이 부여가 되어있는지 아닌지 확인할 수 있습니다.
허용이 되자 않았다면 권한을 허용할 수 있는 설정 페이지를 켜서 사용자가 권한을 허용할 수 있도록 코드를 작성해야합니다.
if (!Settings.canDrawOverlays(this)) {
val builder = StringBuilder()
builder.append("package:$packageName")
val intent = Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse(builder.toString()))
this.startActivity(intent)
}
권한을 획득하게 되면 WindowManager의 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY타입을 사용할 수 있게 됩니다. 이 타입은 우리가 적용할 View 속성에 중요한 역햘을 합니다.
LayoutParams
이제 본격적으로 WindowManager에 등록시킬 View가 보여질 방식에 대해서 설정해보겠습니다. LayoutParams는 View의 속성들을 정의할 수 있습니다. LayoutPararms 다양한 생성자가 있지만, 이 글에서는
public LayoutParams(int w, int h, int xpos, int ypos, int _type, int _flags, int _format) {}
로 생성하려고합니다.
w,h는 View의 width와 height에 해당되고, xpos와 ypos는 이 뷰가 보여질 위치를 말합니다.
다음으로는 이 뷰의 유형을 정해보겠습니다.
fun getWindowManagerLayoutParams() : WindowManager.LayoutParams{
val type: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
} else {
WindowManager.LayoutParams.TYPE_TOAST
}
// flag
}
TYPE_APPLICATION_OVERLAY 속성은 다른 앱 위에서도 작동할 수 있도록 해줍니다.
SYSTEM_ALERT_WINDOW 권한이 없다면 위와 같이 Overlay 타입을 지정할 때 오류가 발생합니다.
API 26 이전에는 TYPE_TOAST, TYPE_SYSTEM_ERROR, TYPE_SYSTEM_OVERLAY, TYPE_SYSTEM_ALERT 중에 하나를 선택해서, Overlay 기능을 구현했지만, API 26(O) 이상부터는 TYPE_APPLICATION_OVERLAY 권한으로 통일되었습니다.
다음으로는 flag입니다. flag는 이 뷰의 속성들을 정의하는 부분입니다.
- FLAG_LAYOUT_IN_SCREEN
- FLAG_FULL_SCREEN
- FLAG_TRANSLUCENT_STATUS
- FLAG_TRANSLUCENT_NAVIGATION
- FLAG_SHOW_WHEN_LOCKED 등등 여러가지 속성들이 존재합니다.
각각의 잠금화면에 맞는 속성들로 지정해주시면 됩니다.
API 30이 되면서 FLAG들 중에 Deprecated된 FLAG들이 많기 때문에 원하는 속성들을 정의하기 위해서는 Android Developer 문서를 참고하시는 것을 추천합니다.
fun getWindowManagerLayoutParams() : WindowManager.LayoutParams{
val type: Int = //
val flag : Int = ( )
val params = WindowManager.LayoutParams(
point.x, // 전체화면을 원한다면 width를 휴대폰의 Display의 width를!
point.y, // 전체화면을 원한다면 height을 휴대폰의 Display의 height를!
0,
0,
type,
flags,
PixelFormat.TRANSLUCENT
)
params
}
위의 정의한 type과 flags 들과 너비, 높이, view의 위치등을 정의해서 LayoutParams를 생성합니다.
잠금화면이 전체화면이 되어야하기 떄문에 point.x와 point.y는 전체화면의 너비와 높이를 넣어주면 됩니다
마지막으로 NotificationListener Service에서
fun addLockScreen(){
// xml inflate
val inflate = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflate.inflate(R.layout.custom_small_view, null)
windowManager.addView(view, getWindowManagerLayoutParams())
// compose
windowManager.addView(composeView, getWindowManagerLayoutParams())
}
위의 기능들을 function으로 만들어서 실행한다면 잠금화면처럼 View를 보여줄 수 있습니다.!
ComposeView 같은 경우 addView를 할때 ViewTreeObserver에 대한 오류가 발생하기 때문에
위의 글을 참고하면서 구현하시면 좋습니다.
마무리
이 글을 작성하게 된 계기는 잠금화면을 구현하는데 어떤 요소들이 필요한가에 목적이 있습니다. 따라서 코드안에 내용을 구체적으로 적지는 않았습니다. 실제로 잠금 화면을 구현하기 위해서 SystemBar hide 같은 기능들을 사용하기 위해서는 추가적인 코드들과 deprecated된 코드들도 사용해야합니다.
그에 대한 정보를 알고 싶으신 분들은 systemUiVisibility에 관련한 정보들을 찾아보시며, WindowInsetsController 와 LayoutCutOutMode에 관련한 정보를 찾아보신다면 구현하시는데 더욱 도움이 될실 것이라고 생각됩니다.
출처
https://stackoverflow.com/questions/9451755/what-is-an-android-window
https://developer.android.com/reference/android/view/ViewGroup.LayoutParams
https://developer.android.com/reference/android/view/WindowManager
'Android' 카테고리의 다른 글
setContentView in Activity (0) | 2023.03.02 |
---|---|
Activity (0) | 2023.02.22 |
Notification을 수신해보자! (0) | 2022.12.08 |
LiveData를 뜯어보자 (0) | 2022.11.23 |
Coroutine이 무엇일까요? (0) | 2022.11.10 |