백그라운드 상태의 앱이 포그라운드 서비스 이용중일때 사용자에게 알림과 동시에 앱의 종료를 막기위한 보조장치로 사용하였다.

 

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으로 이동 및 삭제 

 

+ Recent posts