백그라운드 상태의 앱이 포그라운드 서비스 이용중일때 사용자에게 알림과 동시에 앱의 종료를 막기위한 보조장치로 사용하였다.
SYSTEM__ALERT_WINDOW 기능을 이용한 라이브러리를 사용.
또한 포그라운드와 백그라운드 상태를 받기위하여 라이프사이클 프로세스 사용.
gradle
//floatingview
implementation 'com.github.recruit-lifestyle:FloatingView:2.4.4'
//라이프사이클 프로세스
implementation "androidx.lifecycle:lifecycle-runtime:2.0.0"
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
manifest
sdk 33이상은 post_notifications 권한을 받아야한다.
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
앱 플러팅을 띄울 이미지뷰
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:background="@drawable/bg_main_box"
android:scaleType="center"
android:src="@mipmap/app_icon"></ImageView>
</LinearLayout>
service
class FloatingViewService : Service(), FloatingViewListener {
var mFloatingViewManager: FloatingViewManager? = null
var builder: NotificationCompat.Builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_LOCATION)
var notiText = ""
val EXTRA_CUTOUT_SAFE_AREA ="test"
override fun onBind(intent: Intent?): IBinder? {
TODO("Not yet implemented")
}
//사용자가 x 버튼에 넣었을때 콜백
override fun onFinishFloatingView() {
stopSelf()
}
//사용자가 아이콘을 들었다놨따 할때 위치
override fun onTouchFinished(isFinishing: Boolean, x: Int, y: Int) {
}
override fun onDestroy() {
stopForeground(2)
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("보리 action", intent?.action.toString())
if(intent?.action=="stopFloating"){
stopForeground(2)
stopSelf()
}else{
if (mFloatingViewManager != null) {
return START_STICKY
}
val inflater = LayoutInflater.from(this)
val iconView = inflater.inflate(R.layout.floating_view, null, false)
mFloatingViewManager = FloatingViewManager(this, this)
mFloatingViewManager!!.setFixedTrashIconImage(R.drawable.ico_trash)
mFloatingViewManager!!.setActionTrashIconImage(R.drawable.ico_trash)
mFloatingViewManager!!.setSafeInsetRect(intent!!.getParcelableExtra(EXTRA_CUTOUT_SAFE_AREA));
mFloatingViewManager!!.setDisplayMode(FloatingViewManager.DISPLAY_MODE_SHOW_ALWAYS);
val options = loadOptions()
mFloatingViewManager!!.addViewToWindow(iconView, options)
notiText = "floating"
startForeground(2, getNotification(notiText))
iconView.setOnClickListener {
Log.d("보리 action " , intent?.action.toString())
Toast.makeText(this@FloatingViewService, "앱으로 이동합니다.", Toast.LENGTH_SHORT).show()
var mainIntent = Intent(this, MainActivity::class.java).let { mainIntent ->
mainIntent.putExtra(INTENT_KEY_CLICK_NOTI, true)
mainIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
mainIntent.setAction(ACTION_STOP_FLOATING)
}
// Kotlin
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, mainIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
} else {
PendingIntent.getActivity(this, 0, mainIntent, 0)
}
intent.send()
if (iconView.parent != null) mFloatingViewManager!!.removeAllViewToWindow()
stopSelf()
stopForeground(2)
}
}
return START_REDELIVER_INTENT
}
private fun loadOptions(): FloatingViewManager.Options?{
val options = FloatingViewManager.Options()
options.shape = FloatingViewManager.SHAPE_CIRCLE
options.moveDirection = FloatingViewManager.MOVE_DIRECTION_NONE
options.floatingViewHeight = 289
val defaultX = (Math.random() * 1000).toInt()
val defaultY = (Math.random() * 3000).toInt()
options.floatingViewX = defaultX
options.floatingViewY = defaultY
return options
}
private fun getNotification(text: String): Notification? {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
val pendingIntent =
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_LOCATION)
.setSmallIcon(R.drawable.ico_car_bedge)
.setContentTitle(getString(R.string.app_name))
.setContentText(text)
.setContentIntent(pendingIntent)
.setAutoCancel(false)
return builder.build()
}
}
-헬창코딩님의 블로그 참조.
백그라운드의 상태를 체크하기위한 리스너
class CycleListne(activity:Activity) : LifecycleObserver {
val context = activity
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onMoveToFoground() {
// stopFloatingViewService(this.context,true)
Log.d("보리 lifecyclerObserver","foground"+context.localClassName)
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onMoveToBackground() {
startFloatingViewService(this.context,true)
Log.d("보리 lifecyclerObserver","background"+context.localClassName)
}
//권한이 승인되었다면 호출됨
fun startFloatingViewService(activity: Activity, isCustomFloatingView: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (activity.window.attributes.layoutInDisplayCutoutMode == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) {
throw RuntimeException("'windowLayoutInDisplayCutoutMode' do not be set to 'never'")
}
}
// launch service
val service: Class<out Service?>
val key: String
if (isCustomFloatingView) {
service = FloatingViewService::class.java
key = "12"
} else {
service = FloatingViewService::class.java
key = "12"
}
val intent = Intent(activity, service)
intent.putExtra(key, FloatingViewManager.findCutoutSafeArea(activity))
intent.action = null
ContextCompat.startForegroundService(activity, intent)
}
}
플러팅뷰를 사용할 공통 activity
showFloatingView(this,true,true)
플러팅뷰 권한체크 및 동작
//floatingView
fun showFloatingView(context: Context, isShowOverlayPermission: Boolean, isCustomFloatingView: Boolean) {
// API22
val CHATHEAD_OVERLAY_PERMISSION_REQUEST_CODE = 100
val CUSTOM_OVERLAY_PERMISSION_REQUEST_CODE = 101
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
return
}
// 다른앱위에 표시 할수 있는지 체크
if (Settings.canDrawOverlays(context)) {
return
}
// 오버레이 퍼미션 체크
if (isShowOverlayPermission) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.packageName))
startActivityForResult(intent, if (isCustomFloatingView) CUSTOM_OVERLAY_PERMISSION_REQUEST_CODE else CHATHEAD_OVERLAY_PERMISSION_REQUEST_CODE)
}
}
//권한이 승인되었다면 호출됨
fun startFloatingViewService(activity: Activity, isCustomFloatingView: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (activity.window.attributes.layoutInDisplayCutoutMode == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) {
throw RuntimeException("'windowLayoutInDisplayCutoutMode' do not be set to 'never'")
}
}
// launch service
val service: Class<out Service?>
val key: String
if (isCustomFloatingView) {
service = FloatingViewService::class.java
key = "12"
} else {
service = FloatingViewService::class.java
key = "12"
}
val intent = Intent(activity, service)
intent.putExtra(key, FloatingViewManager.findCutoutSafeArea(activity))
intent.action = null
ContextCompat.startForegroundService(activity, intent)
}
라이프사이클 체크 옵저빙
ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleListener)
동작 -> 안드로이드 3가지 버튼 백그라운드로이동관련 을 이용시 라이프 사이클에서 백그라운드 및 포그라운드를 체크 -> 백그라운드시 floatingView 를 띄움 -> 플러팅뷰를 클릭시 main으로 이동 및 삭제
'프로그래밍 > kotlin' 카테고리의 다른 글
android sdk 33 alarmPermission (0) | 2023.12.28 |
---|---|
[kotlin] api 통신시 TimeOut error (0) | 2023.12.18 |
코틀린 recyclerView item position 변경 이벤트 만들기 (0) | 2023.07.27 |
[Kotlin] google TabLayout 레이아웃간의 간격 벌리기 (0) | 2023.07.13 |
[Kotlin] switch track_selector (0) | 2023.07.13 |