爆速でこんな感じなものを作った
Serviceから動けるアイコンを作ってみた
とりあえずしておきたいこと
androidで表示しているレイヤー
android 8.0以下ならTYPE_PHONE
を使っているけど、android 8.0以上ならTYPE_PHONE
やTYPE_PRIORITY_PHONE
が非推奨になったのでTYPE_APPLICATION_OVERLAY
を使う
Permissionをちゃんと取る必要がある
他のアプリの上で表示するにはACTION_MANAGE_OVERLAY_PERMISSION
でユーザーの許可を取る必要がある。
今ユーザーが許可しているかどうかはSettings.canDrawOverlays(context)
で確認するとが出来る。
android 8.0以上でbackground serviceを始まる時要注意
android 8.0でstartForegroundService(Intent)
サービス開始する時onStartCommand
でやる時にstartForeground(Int, Notification)
をやらないと、serviceが強制終了させるので、要注意。
公式のドキュメントはこちら
https://developer.android.com/about/versions/oreo/background?hl=ja#services
主なコード
Fragment
MainFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
//Permissionチェック
if (!hasDrawOverlaysPermission()) {
requestPermission()
}
return inflater.inflate(R.layout.fragment_main, container, false)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
var count = 0
when (requestCode) {
OVERLAY_PERMISSION_REQUEST_CODE -> {
// 「他アプリの上に表示する」設定の取得が画面復帰直後には取得できない為、1秒間(100ms x 10回)設定の変更を監視する
handler.post(object: Runnable {
override fun run() {
if (count > 10 || hasDrawOverlaysPermission()) {
startService()
} else {
count++
handler.postDelayed(this, 100)
}
}
})
}
}
}
private fun hasDrawOverlaysPermission(): Boolean {
return Settings.canDrawOverlays(context)
}
private fun requestPermission() {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context?.packageName}"))
startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE)
}
private fun startService() {
val intent = Intent(context, FloatService::class.java)
// Serviceの開始
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context?.startForegroundService(intent)
} else {
context?.startService(intent)
}
}
Service
FloatMenuService.kt
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
createNotification(intent)
createFloatMenuView()
return super.onStartCommand(intent, flags, startId)
}
private fun createNotification(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "好きなchannelId"
val notificationTitle = "タイトル"
var notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
var pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
notificationManager.createNotificationChannel(NotificationChannel(channelId, title, NotificationManager.IMPORTANCE_DEFAULT))
var notification = Notification.Builder(applicationContext, channelId)
.setContentTitle(notificationTitle)
.setSmallIcon(R.drawable.ic_stat_name)//image assetで生成出来る、Adaptive iconを使うとSystem UIがクラッシュする
.setContentText("notificationの本文")
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.build()
startForeground(ONGOING_NOTIFICATION_ID, notification)//ONGOING_NOTIFICATION_IDは0が使えない
}
}
private fun createFloatMenuView() {
windowManager = applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManagerParams = WindowManager.LayoutParams()
layoutInflater = LayoutInflater.from(this)
windowManagerParams.let {
//レイヤー設定
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
it.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
it.type = WindowManager.LayoutParams.TYPE_PHONE
}
it.format = PixelFormat.RGBA_8888
it.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
it.gravity = Gravity.LEFT or Gravity.TOP
it.x = 0
it.y = 0
it.width = WindowManager.LayoutParams.WRAP_CONTENT
it.height = WindowManager.LayoutParams.WRAP_CONTENT
}
floatMenuLayout = layoutInflater.inflate(R.layout.float_menu_layout, null)
windowManager?.addView(floatMenuLayout, windowManagerParams)
floatMenuView = floatMenuLayout.findViewById(R.id.float_menu_button) as ImageButton
floatMenuLayout.measure(View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
//floatMenuの座標移動
floatMenuView.setOnTouchListener { _, event ->
windowManagerParams.x = event.rawX.toInt() - floatMenuView.measuredWidth / 2
windowManagerParams.y = event.rawY.toInt() - floatMenuView.measuredHeight / 2
mWindowManager?.updateViewLayout(floatMenuLayout, windowManagerParams)
false
}
floatMenuView.setOnClickListener {
//タップした時の処理
}
}
float menuのXML
float_menu_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageButton
android:id="@+id/float_menu_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:gravity="center_horizontal"
android:src="@mipmap/ic_launcher" />
</android.support.constraint.ConstraintLayout>