forked from flutter-moum/flutter_hardware_buttons
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Android] Prevent memory leak from EventChannel.StreamHandler
- Loading branch information
Showing
4 changed files
with
187 additions
and
98 deletions.
There are no files selected for viewing
75 changes: 2 additions & 73 deletions
75
android/src/main/kotlin/flutter/moum/hardware_buttons/HardwareButtonsPlugin.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
158 changes: 158 additions & 0 deletions
158
android/src/main/kotlin/flutter/moum/hardware_buttons/HardwareButtonsWatcherManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
25 changes: 0 additions & 25 deletions
25
android/src/main/kotlin/flutter/moum/hardware_buttons/KeyDetectionView.kt
This file was deleted.
Oops, something went wrong.
27 changes: 27 additions & 0 deletions
27
android/src/main/kotlin/flutter/moum/hardware_buttons/VolumeButtonStreamHandler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |