Skip to content

Commit

Permalink
[Android] Prevent memory leak from EventChannel.StreamHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
giantsol committed Oct 2, 2019
1 parent fd708e3 commit 3a94cb4
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -1,87 +1,16 @@
package flutter.moum.hardware_buttons

import android.app.Activity
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.PluginRegistry


class HardwareButtonsPlugin(
private val activity: Activity,
private val type: HardwareButtonType
): EventChannel.StreamHandler {
class HardwareButtonsPlugin {
companion object {
private const val VOLUME_BUTTON_CHANNEL_NAME = "flutter.moum.hardware_buttons.volume"

@JvmStatic
fun registerWith(registrar: PluginRegistry.Registrar) {
val volumeButtonChannel = EventChannel(registrar.messenger(), VOLUME_BUTTON_CHANNEL_NAME)
volumeButtonChannel.setStreamHandler(HardwareButtonsPlugin(registrar.activity(), HardwareButtonType.VOLUME))
}
}

// todo: 싱글턴으로 만들어야함. 안그러면 HardwareButtonType을 여러개 추가할 때 마다 새로운 windowOverlayView를 추가할테니

private var keyDetectionView: KeyDetectionView? = null

// Flutter에서 앱이 종료될 때 onCancel() 콜백을 제대로 안불러주기 때문에 우리가 직접 불러야함
// related: https://github.com/flutter/plugins/pull/1992/files/04df85fef5a994d93d89b02b27bb7789ec452528#diff-efd825c710217272904545db4b2198e2
private val lifecycleCallbacks = object: EmptyActivityLifecycleCallbacks() {
override fun onActivityDestroyed(activity: Activity?) {
// todo: 파라미터로 넘어온 activity가 우리 activity와 동일한애인지 확인해야함
this@HardwareButtonsPlugin.activity.application.unregisterActivityLifecycleCallbacks(this)
onCancel(null)
volumeButtonChannel.setStreamHandler(VolumeButtonStreamHandler(registrar.activity()))
}
}

init {
activity.application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
}

override fun onListen(args: Any?, eventDispatcher: EventChannel.EventSink?) {
keyDetectionView = KeyDetectionView(activity, callback = object: KeyDetectionView.KeyCallback {
override fun onKeyEvent(event: KeyEvent) {
if (event.action == KeyEvent.ACTION_DOWN) {
if (type == HardwareButtonType.VOLUME &&
(event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) {
eventDispatcher?.success(event.keyCode)
}
}
activity.dispatchKeyEvent(event)
}
})

addOverlayWindowView(keyDetectionView!!)
}

override fun onCancel(args: Any?) {
keyDetectionView?.run { removeOverlayWindowView(this) }
}

private fun addOverlayWindowView(view: View) {
val windowType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

val params = WindowManager.LayoutParams(0, 0,
windowType,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT)

(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).addView(view, params)
}

private fun removeOverlayWindowView(view: View) {
(activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).removeView(view)
}
}

enum class HardwareButtonType {
VOLUME,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package flutter.moum.hardware_buttons

import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.graphics.PixelFormat
import android.os.Build
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager


// singleton object for managing various resources related with getting hardware button events.
// those who need to listen to any hardware button events add listener to this single instance.
// e.g. HardwareButtonsWatcherManager.getInstance(application, activity).addVolumeButtonListener(volumeButtonListener)
@SuppressLint("StaticFieldLeak")
object HardwareButtonsWatcherManager {
interface VolumeButtonListener {
fun onVolumeButtonEvent(event: VolumeButtonEvent)
}
enum class VolumeButtonEvent(val value: Int) {
VOLUME_UP(24),
VOLUME_DOWN(25),
}

private var application: Application? = null
private var currentActivity: Activity? = null
private var activityLifecycleCallbacks: Application.ActivityLifecycleCallbacks? = null

private var keyWatcher: KeyWatcher? = null
private var volumeButtonListeners: ArrayList<VolumeButtonListener> = arrayListOf()

fun getInstance(application: Application, activity: Activity): HardwareButtonsWatcherManager {
this.application = application
// set currentActivity to activity only when ActivityLifecycleCallbacks wasn't registered yet.
// otherwise, currentActivity will be updated in ActivityLifecycleCallbacks.
if (activityLifecycleCallbacks == null) {
currentActivity = activity
}
registerActivityLifecycleCallbacksIfNeeded()
return this
}

private fun registerActivityLifecycleCallbacksIfNeeded() {
if (activityLifecycleCallbacks == null) {
activityLifecycleCallbacks = object: EmptyActivityLifecycleCallbacks() {
override fun onActivityStarted(activity: Activity?) {
currentActivity = activity

// attach necessary watchers
attachKeyWatcherIfNeeded()
}

override fun onActivityStopped(activity: Activity?) {
if (currentActivity?.equals(activity) == true) {
// detach all watchers
detachKeyWatcher()
}
}

override fun onActivityDestroyed(activity: Activity?) {
if (currentActivity?.equals(activity) == true) {
currentActivity = null

// remove all listeners and detach all watchers
// When flutter app finishes, it doesn't invoke StreamHandler's onCancel() callback properly, so
// we should manually clean up resources (i.e. listeners) when activity state becomes invalid (in order to avoid memory leak).
// related: https://github.com/flutter/plugins/pull/1992/files/04df85fef5a994d93d89b02b27bb7789ec452528#diff-efd825c710217272904545db4b2198e2
volumeButtonListeners.clear()
detachKeyWatcher()
}
}
}
application?.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
}
}

fun addVolumeButtonListener(listener: VolumeButtonListener) {
if (!volumeButtonListeners.contains(listener)) {
volumeButtonListeners.add(listener)
}
attachKeyWatcherIfNeeded()
}

fun removeVolumeButtonListener(listener: VolumeButtonListener) {
volumeButtonListeners.remove(listener)
if (volumeButtonListeners.size == 0) {
detachKeyWatcher()
}
}

private fun attachKeyWatcherIfNeeded() {
val application = application ?: return
if (volumeButtonListeners.size > 0 && keyWatcher == null) {
keyWatcher = KeyWatcher(application.applicationContext, callback = {
dispatchVolumeButtonEvent(it)

currentActivity?.dispatchKeyEvent(it)
})
addOverlayWindowView(application, keyWatcher!!)
}
}

private fun dispatchVolumeButtonEvent(keyEvent: KeyEvent) {
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
val volumeButtonEvent = when (keyEvent.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> VolumeButtonEvent.VOLUME_UP
KeyEvent.KEYCODE_VOLUME_DOWN -> VolumeButtonEvent.VOLUME_DOWN
else -> null
}
if (volumeButtonEvent != null) {
for (listener in volumeButtonListeners) {
listener.onVolumeButtonEvent(volumeButtonEvent)
}
}
}
}

private fun addOverlayWindowView(context: Context, view: View) {
val windowType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

val params = WindowManager.LayoutParams(0, 0,
windowType,
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSLUCENT)

(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).addView(view, params)
}

private fun detachKeyWatcher() {
val application = application ?: return
val keyWatcher = keyWatcher ?: return
removeOverlayWindowView(application, keyWatcher)
this.keyWatcher = null
}

private fun removeOverlayWindowView(context: Context, view: View) {
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).removeView(view)
}
}

// simple view just to override dispatchKeyEvent
private class KeyWatcher @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
private val callback: ((event: KeyEvent) -> Unit)? = null
) : View(context, attrs, defStyleAttr) {
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
callback?.invoke(event)
return false
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package flutter.moum.hardware_buttons

import android.app.Activity
import io.flutter.plugin.common.EventChannel


class VolumeButtonStreamHandler(private val activity: Activity): EventChannel.StreamHandler {
private val application = activity.application
private var streamSink: EventChannel.EventSink? = null

private val volumeButtonListener = object: HardwareButtonsWatcherManager.VolumeButtonListener {
override fun onVolumeButtonEvent(event: HardwareButtonsWatcherManager.VolumeButtonEvent) {
streamSink?.success(event.value)
}
}

override fun onListen(args: Any?, sink: EventChannel.EventSink?) {
this.streamSink = sink
HardwareButtonsWatcherManager.getInstance(application, activity).addVolumeButtonListener(volumeButtonListener)
}

// this function doesn't actually get called by flutter framework as of now: 2019/10/02
override fun onCancel(args: Any?) {
this.streamSink = null
HardwareButtonsWatcherManager.getInstance(application, activity).removeVolumeButtonListener(volumeButtonListener)
}
}

0 comments on commit 3a94cb4

Please sign in to comment.