Skip to content

Instantly share code, notes, and snippets.

@nosix
Last active September 24, 2021 06:42
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nosix/c3baf62bb095410a812ac638413c457d to your computer and use it in GitHub Desktop.
Save nosix/c3baf62bb095410a812ac638413c457d to your computer and use it in GitHub Desktop.
Floating App for Android (SDK 21) in Kotlin 1.0.3
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xxx">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
...
<service android:name=".FloatingAppService"/>
</application>
</manifest>
package xxx
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
fun Activity.hasOverlayPermission(): Boolean =
if (Build.VERSION.SDK_INT >= 23) Settings.canDrawOverlays(this) else true
fun Activity.requestOverlayPermission(requestCode: Int) {
if (Build.VERSION.SDK_INT >= 23) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
startActivityForResult(intent, requestCode)
}
}
package xxx
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.view.WindowManager
import android.widget.ImageView
import java.util.*
import xxx.FloatingButton
class FloatingAppService : Service() {
companion object {
val ACTION_START = "start"
val ACTION_STOP = "stop"
}
private val notificationId = Random().nextInt()
private var button: FloatingButton? = null
override fun onCreate() {
super.onCreate()
startNotification()
}
private fun startNotification() {
val activityIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, 0)
val notification = Notification.Builder(this)
.setContentIntent(pendingIntent)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(FloatingAppService::class.simpleName)
.setContentText("Service is running.")
.build()
startForeground(notificationId, notification)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null || intent.action == ACTION_START) {
startOverlay()
} else {
stopSelf()
}
return Service.START_STICKY
}
override fun onDestroy() {
super.onDestroy()
stopOverlay()
}
private fun startOverlay() {
ImageView(this).run {
val windowManager = getSystemService(Service.WINDOW_SERVICE) as WindowManager
setImageResource(android.R.drawable.ic_menu_add)
button = FloatingButton(windowManager, this).apply {
visible = true
}
}
}
private fun stopOverlay() {
button?.run {
visible = false
}
button = null
}
}
package xxx
import android.graphics.PixelFormat
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
class FloatingButton(val windowManager: WindowManager, val view: View) {
companion object {
private val TAG = FloatingButton::class.qualifiedName
}
private val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT)
.apply {
gravity = Gravity.TOP or Gravity.START
x = 100
y = 100
}
var visible: Boolean = false
set(value) {
if (field != value) {
field = value
if (value) {
windowManager.addView(view, params)
} else {
windowManager.removeView(view)
}
}
}
private var initial: Position? = null
init {
view.setOnTouchListener {
view, e ->
when (e.action) {
MotionEvent.ACTION_DOWN -> {
initial = params.position - e.position
}
MotionEvent.ACTION_MOVE -> {
initial?.let {
params.position = it + e.position
windowManager.updateViewLayout(view, params)
}
}
MotionEvent.ACTION_UP -> {
initial = null
}
}
false
}
view.setOnClickListener {
Log.d(TAG, "onClick")
}
}
private val MotionEvent.position: Position
get() = Position(rawX, rawY)
private var WindowManager.LayoutParams.position: Position
get() = Position(x.toFloat(), y.toFloat())
set(value) {
x = value.x
y = value.y
}
private data class Position(val fx: Float, val fy: Float) {
val x: Int
get() = fx.toInt()
val y: Int
get() = fy.toInt()
operator fun plus(p: Position) = Position(fx + p.fx, fy + p.fy)
operator fun minus(p: Position) = Position(fx - p.fx, fy - p.fy)
}
}
package xxx
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.MotionEvent
import xxx.FloatingAppService
import xxx.hasOverlayPermission
import xxx.requestOverlayPermission
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = MainActivity::class.qualifiedName
private val REQUEST_OVERLAY_PERMISSION = 1
}
private var enabled = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
if (hasOverlayPermission()) {
val intent = Intent(this, FloatingAppService::class.java)
.setAction(FloatingAppService.ACTION_STOP)
startService(intent)
} else {
requestOverlayPermission(REQUEST_OVERLAY_PERMISSION)
}
}
override fun onStop() {
super.onStop()
if (enabled && hasOverlayPermission()) {
val intent = Intent(this, FloatingAppService::class.java)
.setAction(FloatingAppService.ACTION_START)
startService(intent)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
when (requestCode) {
REQUEST_OVERLAY_PERMISSION -> Log.d(TAG, "enable overlay permission")
}
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
enabled = false
return super.onTouchEvent(event)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment