How to turn BroadcastReceiver into StreamHandler while writing native plugin? - android

I've never written android code before. Now I want to write a Flutter plugin that collects Wi-Fi data.
Following this guide and found a video on YouTube but author uses StreamHandler, but I couldn't find a way to implement StreamHandler related code.
Wrote these code so far in the kotlin side, as my understanding I have to use wifiScanReceiver as StreamHandler but it is a BroadcastReceiver. Therefore the dataChannel!!.setStreamHandler(wifiScanReceiver) gives error.
class MainActivity(wifiManager: WifiManager) : FlutterActivity() {
private val METHOD_CHANNEL_NAME = "com.baran.collect_wifi/method"
private val DATA_CHANNEL_NAME = "com.baran.collect_wifi/data"
private var methodChannel : MethodChannel? = null
private lateinit var wifiManager: WifiManager
private var dataChannel : EventChannel? = null
private var eventSink: EventChannel.EventSink? = null
#RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
//Setup channels
setupChannels(this, flutterEngine.dartExecutor.binaryMessenger)
}
override fun onDestroy() {
teardownChannels()
super.onDestroy()
}
private val wifiScanReceiver = object : BroadcastReceiver() {
#RequiresApi(Build.VERSION_CODES.M)
override fun onReceive(context: Context, intent: Intent) {
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
if (success) {
scanSuccess()
} else {
scanFailure()
}
}
}
private fun scanSuccess() {
}
private fun scanFailure() {
// handle failure: new scan did NOT succeed
// consider using old scan results: these are the OLD results!
val results = wifiManager.scanResults
}
#RequiresApi(Build.VERSION_CODES.M)
private fun setupChannels(context: Context, messenger: BinaryMessenger){
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)
methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
methodChannel!!.setMethodCallHandler{
call, _ ->
if(call.method == "startScan"){
val success = wifiManager.startScan()
if (!success) {
// scan failure handling
scanFailure()
}
}
}
dataChannel = EventChannel(messenger, DATA_CHANNEL_NAME)
dataChannel!!.setStreamHandler(wifiScanReceiver)
}
private fun teardownChannels() {
methodChannel!!.setMethodCallHandler(null)
}
}
And this is the flutter code so far
import 'dart:async';
import 'package:flutter/services.dart';
class CollectWifi {
static const _channel = MethodChannel('com.baran.collect_wifi/method');
static const _dataChannel = EventChannel('com.baran.collect_wifi/data');
}
I don't know if I'm going correctly

you can not pass WifiManager in to setStreamHandler
Activity does not take WifiManager as input parameter
I updated some point in your source code as below
class MainActivity : FlutterActivity() {
private val METHOD_CHANNEL_NAME = "com.baran.collect_wifi/method"
private val DATA_CHANNEL_NAME = "com.baran.collect_wifi/data"
private var methodChannel: MethodChannel? = null
private var dataChannel: EventChannel? = null
private var eventSink: EventChannel.EventSink? = null // using send data to flutter layer
val wifiManager = application.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiScanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false)
if (success) {
scanSuccess()
} else {
scanFailure()
}
}
}
private fun scanSuccess() {
val results = wifiManager.scanResults
eventSink?.success("Wifi Connected")// send event to flutter layer
}
private fun scanFailure() {
// handle failure: new scan did NOT succeed
// consider using old scan results: these are the OLD results!
val results = wifiManager.scanResults
eventSink?.error("404","Wifi Not connected",null) // send event to flutter layer
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)
val success = wifiManager.startScan()
if (!success) {
// scan failure handling
scanFailure()
}
}
override fun onDestroy() {
unregisterReceiver(wifiScanReceiver) // remember unRegister to prevent memory leak
super.onDestroy()
}
#RequiresApi(Build.VERSION_CODES.M)
private fun setupChannels(context: Context, messenger: BinaryMessenger){
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
context.registerReceiver(wifiScanReceiver, intentFilter)
methodChannel = MethodChannel(messenger, METHOD_CHANNEL_NAME)
methodChannel!!.setMethodCallHandler{
call, _ ->
if(call.method == "startScan"){
val success = wifiManager.startScan()
if (!success) {
// scan failure handling
scanFailure()
}
}
}
dataChannel = EventChannel(messenger, DATA_CHANNEL_NAME)
//dataChannel.setStreamHandler(setStreamHandler(wifiScanReceiver)
dataChannel?.setStreamHandler(object :EventChannel.StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events // object using send data to flutter layer
}
override fun onCancel(arguments: Any?) {
}
})
}
private fun teardownChannels() {
methodChannel!!.setMethodCallHandler(null)
}
}

Related

how do i get the set notification in BLE nordic

i am trying to set notification callback in BLE nordic, where i am using the Android BLE library (Nordic Github). But i not ablet to get the notification event when i am changing the value of the characteristics.
`
class BleManagerHP1T(context: Context) : BleManager(context) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, GattService.TAG, message)
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
private var rxCharacteristic: BluetoothGattCharacteristic? = null
#SuppressLint("MissingPermission")
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(GattService.MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(GattService.MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
Log.d(TAG, "isRequiredServiceSupported: notify ${(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)}")
rxCharacteristic = service?.getCharacteristic(GattService.MyServiceProfile.RX_CHARACTERISTIC_UUID)
val obj = JSONObject()
obj.put("OPCODE","PROVISION")
rxCharacteristic?.value = obj.toString().encodeToByteArray()
val rxRead = gatt.writeCharacteristic(rxCharacteristic)
Log.d(TAG, "isRequiredServiceSupported: Read $rxRead")
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
enableNotifications(myCharacteristic).enqueue()
requestMtu(260).enqueue();
setNotificationCallback(myCharacteristic).with { _, data ->
Log.d(TAG, "initialize: TX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: TX char value $value")
}
}
requestMtu(260).enqueue();
enableNotifications(rxCharacteristic).enqueue()
setNotificationCallback(rxCharacteristic).with { _, data ->
Log.d(TAG, "initialize: RX char Notification Called")
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
Log.d(TAG, "initialize: RX char value $value")
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
override fun readCharacteristic(characteristic: BluetoothGattCharacteristic?): ReadRequest {
return Request.newReadRequest(characteristic)
}
}
`
gatt connection is establishing perfectly fine, using this code.
`
val bleManager = BleManagerHP1T(this#ControllerActivity)
synchronized (this) {
bleManager.connect(deviceMainList[position]).useAutoConnect(false).enqueue()
}
`
here is the gatt service file .
`
class GattService : Service() {
private val defaultScope = CoroutineScope(Dispatchers.Default)
private lateinit var bluetoothObserver: BroadcastReceiver
private var myCharacteristicChangedChannel: SendChannel<String>? = null
private val clientManagers = mutableMapOf<String, ClientManager>()
// val connect = BleManager()
#RequiresApi(Build.VERSION_CODES.O)
override fun onCreate() {
super.onCreate()
// Setup as a foreground service
val notificationChannel = NotificationChannel(
GattService::class.java.simpleName,
resources.getString(R.string.gatt_service_name),
NotificationManager.IMPORTANCE_DEFAULT
)
val notificationService =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationService.createNotificationChannel(notificationChannel)
val notification = NotificationCompat.Builder(this, GattService::class.java.simpleName)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(resources.getString(R.string.gatt_service_name))
.setContentText(resources.getString(R.string.gatt_service_running_notification))
.setAutoCancel(true)
startForeground(1, notification.build())
// Observe OS state changes in BLE
bluetoothObserver = object : BroadcastReceiver() {
#SuppressLint("MissingPermission")
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
val bluetoothState = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
-1
)
when (bluetoothState) {
BluetoothAdapter.STATE_ON -> enableBleServices()
BluetoothAdapter.STATE_OFF -> disableBleServices()
}
}
BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
val device =
intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.d(TAG, "Bond state changed for device ${device?.address}: ${device?.bondState}")
when (device?.bondState) {
BluetoothDevice.BOND_BONDED -> addDevice(device)
BluetoothDevice.BOND_NONE -> removeDevice(device)
}
}
}
}
}
registerReceiver(bluetoothObserver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
registerReceiver(bluetoothObserver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
// Startup BLE if we have it
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) enableBleServices()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(bluetoothObserver)
disableBleServices()
}
override fun onBind(intent: Intent?): IBinder? =
when (intent?.action) {
DATA_PLANE_ACTION -> {
DataPlane()
}
else -> null
}
override fun onUnbind(intent: Intent?): Boolean =
when (intent?.action) {
DATA_PLANE_ACTION -> {
myCharacteristicChangedChannel = null
true
}
else -> false
}
/**
* A binding to be used to interact with data of the service
*/
inner class DataPlane : Binder() {
fun setMyCharacteristicChangedChannel(sendChannel: SendChannel<String>) {
myCharacteristicChangedChannel = sendChannel
}
}
#SuppressLint("MissingPermission")
private fun enableBleServices() {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
if (bluetoothManager.adapter?.isEnabled == true) {
Log.i(TAG, "Enabling BLE services")
bluetoothManager.adapter.bondedDevices.forEach { device -> addDevice(device) }
} else {
Log.w(TAG, "Cannot enable BLE services as either there is no Bluetooth adapter or it is disabled")
}
}
private fun disableBleServices() {
clientManagers.values.forEach { clientManager ->
clientManager.close()
}
clientManagers.clear()
}
private fun addDevice(device: BluetoothDevice) {
if (!clientManagers.containsKey(device.address)) {
val clientManager = ClientManager()
clientManager.connect(device).useAutoConnect(true).enqueue()
clientManagers[device.address] = clientManager
}
}
private fun removeDevice(device: BluetoothDevice) {
clientManagers.remove(device.address)?.close()
}
/*
* Manages the entire GATT service, declaring the services and characteristics on offer
*/
companion object {
/**
* A binding action to return a binding that can be used in relation to the service's data
*/
const val DATA_PLANE_ACTION = "data-plane"
const val TAG = "gatt-service"
}
private inner class ClientManager : BleManager(this#GattService) {
override fun getGattCallback(): BleManagerGattCallback = GattCallback()
override fun log(priority: Int, message: String) {
if (BuildConfig.DEBUG || priority == Log.ERROR) {
Log.println(priority, TAG, message)
Log.d(TAG, "log: $message")
}
}
private inner class GattCallback : BleManagerGattCallback() {
private var myCharacteristic: BluetoothGattCharacteristic? = null
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(MyServiceProfile.MY_SERVICE_UUID)
myCharacteristic =
service?.getCharacteristic(MyServiceProfile.MY_CHARACTERISTIC_UUID)
val myCharacteristicProperties = myCharacteristic?.properties ?: 0
return (myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_READ != 0) &&
(myCharacteristicProperties and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0)
}
override fun initialize() {
setNotificationCallback(myCharacteristic).with { _, data ->
if (data.value != null) {
val value = String(data.value!!, Charsets.UTF_8)
defaultScope.launch {
myCharacteristicChangedChannel?.send(value)
}
}
}
beginAtomicRequestQueue()
.add(enableNotifications(myCharacteristic)
.fail { _: BluetoothDevice?, status: Int ->
log(Log.ERROR, "Could not subscribe: $status")
disconnect().enqueue()
}
)
.done {
log(Log.INFO, "Target initialized")
}
.enqueue()
}
override fun onServicesInvalidated() {
myCharacteristic = null
}
}
}
object MyServiceProfile {
val MY_SERVICE_UUID: UUID = UUID.fromString("8d67d51a-801b-43cb-aea2-bbec9d1211fd")
val MY_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51c-801b-43cb-aea2-bbec9d1211fd")
val RX_CHARACTERISTIC_UUID: UUID = UUID.fromString("8d67d51b-801b-43cb-aea2-bbec9d1211fd")
}
}
`

CustomTabsClient.bindCustomTabsService always return false

I try to implement Custom Tab Intent with warmup capability. So it needs to create CustomTabsServiceConnection.
However, when I bind the service it always return false.
Please check my implementation below:
// Custom chrome tabs
private lateinit var customTabsIntent: CustomTabsIntent
private lateinit var customTabsClient: CustomTabsClient
private var connection: CustomTabsServiceConnection? = null
override fun onStart() {
super.onStart()
val ok = CustomTabsClient.bindCustomTabsService(this, CUSTOM_TAB_PACKAGE_NAME, ServiceConnection())
Timber.i("Is OK = $ok")
}
inner class ServiceConnection: CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(
name: ComponentName,
client: CustomTabsClient
) {
customTabsClient = client
customTabsClient.warmup(0)
// Connection callback
val session = customTabsClient.newSession(object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
super.onNavigationEvent(navigationEvent, extras)
Timber.i("Event = $navigationEvent")
}
})
if (session != null) {
session.mayLaunchUrl(Uri.parse(loginUrl), null, null)
// Init custom tab intent
val customTabBuilder = CustomTabsIntent.Builder(session)
customTabsIntent = customTabBuilder.build()
customTabsIntent.launchUrl(this, Uri.parse(loginUrl))
}
}
override fun onServiceDisconnected(name: ComponentName?) {
connection = null
}
}
override fun onStop() {
super.onStop()
connection?.let {
this.unbindService(it)
}
}
Any idea about this?

Android FragmentManager has not been attached to a host

I need to create a Flutter plugin and link native Android code from ADA bot. I have no experience with android but by following the documentation, it should be simple:
val adaView = AdaEmbedView(getContext())
val adaSettings = AdaEmbedView.Settings.Builder("ada-example").build()
adaView.initialize(adaSettings)
val dialog = AdaEmbedDialog()
adaDialog.arguments = Bundle().apply {
putParcelable(AdaEmbedDialog.ARGUMENT_SETTINGS, settings)
}
dialog.show(supportFragmentManager, AdaEmbedDialog.TAG)
When I run this code I get the error:
Failed to handle method call
java.lang.IllegalStateException: FragmentManager has not been attached to a host.
I write the code in the flutter create plugin autogenerated file FlutterAdaPlugin.kt and it looks like this:
public class FlutterAdaPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener, FragmentActivity() {
private lateinit var methodChannel : MethodChannel
private lateinit var eventChannel : EventChannel
private lateinit var eventSink : EventChannel.EventSink
private lateinit var adaFramerwork: AdaEmbedView
private lateinit var applicationCnt: Context
private var activity: Activity? = null
private lateinit var addaSettings: AdaEmbedView.Settings
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) : Boolean {
super.onActivityResult(requestCode, resultCode, data)
return true
}
override fun onAttachedToEngine(#NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.applicationCnt = applicationContext
}
private fun onAttachedToEngine(applicationContext: Context, messenger: BinaryMessenger) {
this.applicationCnt = applicationContext
methodChannel = MethodChannel(messenger, "FlutterAdaPlugin")
eventChannel = EventChannel(messenger, "FlutterAdaPluginStream")
methodChannel.setMethodCallHandler(this)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(listener: Any?, eventSink: EventChannel.EventSink) {
this#FlutterAdaPlugin.eventSink = eventSink;
}
})
}
companion object {
#JvmStatic
fun registerWith(registrar: Registrar) {
val plugin = FlutterAdaPlugin()
plugin.activity = registrar.activity()
registrar.addActivityResultListener(plugin)
plugin.onAttachedToEngine(registrar.context(), registrar.messenger())
}
}
override fun onMethodCall(#NonNull call: MethodCall, #NonNull result: Result) {
when (call.method) {
"launchNavWebSupport" -> launchNavWebSupport()
}
else -> result.notImplemented()
}
}
private fun launchNavWebSupport() {
setupAdaFramework()
val dialog = AdaEmbedDialog()
dialog.arguments = Bundle().apply {
putParcelable(AdaEmbedDialog.ARGUMENT_SETTINGS, addaSettings)
}
if(supportFragmentManager != null) {
}
dialog.show(supportFragmentManager, AdaEmbedDialog.TAG)
}
private fun setupAdaFramework() {
val activity: Context = activity?.takeIf { activity != null } ?: return
adaFramerwork = AdaEmbedView(activity)
addaSettings = AdaEmbedView.Settings.Builder("test")
.build()
adaFramerwork.initialize(addaSettings)
}
override fun onDetachedFromEngine(#NonNull binding: FlutterPlugin.FlutterPluginBinding) {
eventChannel.setStreamHandler(null)
methodChannel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivity() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
}

Passing data from a class which is Lifecycle observer to the Activity

I have a class BatteryInfo which is a life-cycle observer and in this class, there is a BroadCastReceiver which is responsible for getting all battery information. This class is working perfectly with the lifecycle of Activity from where I have called it. This means it is registering the broadcast on activity created and unRegister on closing activity. But I am confused about how to access this broadcast live information in Activity.
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle,
): LifecycleObserver {
private var _enabled = false
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
// Log.d("TAG", String.format("%.1f", voltage / 1000f) + " V")
}
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// if (_enabled) {
_context.registerReceiver(broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
// }
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
}
// connect if not connected
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
I am Calling this class from MainActivity onCreate method like this
BatteryInfo(this, lifecycle)
A simple listener would work :
data class Stats(val level : Int,
val temp : Int,
val voltage : Int,
val technology : Int,
val plugged : Int,
val health : Int)
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle
): LifecycleObserver {
private var _enabled = false
private var listener: ((Stats) -> Unit)? = null
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
listener?.invoke(Stats(level, temperature, voltage, technology, plugged, health))
// Log.d("TAG", String.format("%.1f", voltage / 1000f) + " V")
}
}
}
fun setListener(listener: ((Stats) -> Unit)?) {
this.listener = listener
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
// if (_enabled) {
_context.registerReceiver(broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
// }
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
listener = null
}
// connect if not connected
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
class SomeActivity : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BatteryInfo(this, lifecycle)
.apply { setListener { stats: Stats -> Log.d(TAG, stats.toString()) } }
}
}
I know its not you answer but its a different approach of getting things done. It also removes all of your boilerplate code.
You can use an object to extract all your data from intent and return an instance of your data class.
Read more about object in kotlin here - https://kotlinlang.org/docs/tutorials/kotlin-for-py/objects-and-companion-objects.html
object BatteryInfoUtils {
fun getBatteryInfoFromIntent(intent: Intent): Stats { intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
return Stats(level, temperature, voltage, technology, plugged, health)
}
}
And inside MainActivity onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val stats = getBatteryInfo()
}
fun getBatteryInfo(): Stats? {
var stats: Stats? = null
val intent: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
registerReceiver(null, ifilter)
}
intent?.let { batteryIntent ->
stats = BatteryInfoUtils.getBatteryInfoFromIntent(batteryIntent)
}
return stats
}
I have done this with the help of MutableLiveData & LiveData but I am not sure it is a perfect solution or a hacky solution. I want to do a perfect solution that follows MVVM, clean-architecture, and SOLID principle without the creation of multiple objects because it is very costly.
class BatteryInfo(
private val _context: Context,
private val _lifecycle: Lifecycle
) : LifecycleObserver {
val map = HashMap<String, Int>()
private var _enabled = false
private val _batteryInfoMap = MutableLiveData<HashMap<String, Int>>()
val batteryInfo: LiveData<HashMap<String, Int>>
get() = _batteryInfoMap
init {
_lifecycle.addObserver(this)
}
private val broadcastBatteryInfoListener = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.let {
val level = it.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val temperature = it.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, -1)
val voltage = it.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1)
val technology = it.getIntExtra(BatteryManager.EXTRA_TECHNOLOGY, -1)
val plugged = it.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
val health = it.getIntExtra(BatteryManager.EXTRA_HEALTH, -1)
map["level"] = level
map["temperature"] = temperature
_batteryInfoMap.value = map
}
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
if (_enabled) {
_context.registerReceiver(
broadcastBatteryInfoListener,
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
)
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
_context.unregisterReceiver(broadcastBatteryInfoListener)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun destroy() {
_lifecycle.removeObserver(this)
}
fun enable() {
_enabled = true
if (_lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
start()
}
}
}
And inside MainActivity onCreate()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val into = BatteryInfo(this, lifecycle)
into.enable()
into.batteryInfo.observe(this, Observer {
Log.d("TAG", "${it.values}")
})
}

sendBroadcast from inside Service which uses Coroutine for network call

I have a JobIntentService which is supposed to do an API call and do a broadcast once the result is available.
I am using a Coroutine to do the network call using Retrofit.
However, if I do sendBroadcast within the CoroutineScope , it does not trigger the BroadcastReceiver
This is my service code -
MyService.kt
class MyService : JobIntentService() {
private val TAG = MyService::class.java.simpleName
private var databaseHelper: DatabaseHelper = DatabaseHelper(this)
private var imageFetcher: ImageFetcher = ImageFetcher(this)
private var imageSaver: ImageSaver = ImageSaver(this)
private val receiver = ServiceBroadcastReceiver()
override fun onHandleWork(intent: Intent) {
val filter = IntentFilter()
filter.addAction("ACTION_FINISHED_SERVICE")
registerReceiver(receiver, filter)
when (intent.action) {
"ACTION_FETCH_FROM_API" -> {
handleFetchFromAPI()
}
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver)
}
private fun handleFetchFromAPI() {
val API = ServiceBuilder.buildWebService(WebService::class.java)
CoroutineScope(IO).launch {
try {
var apiSuccess : Boolean = false
val apiResponse = API.getImageOfTheDay()
if (apiResponse.isSuccessful) {
apiSuccess = true
val imageAPIResponse = apiResponse.body()
val bitmap = imageFetcher.getImageBitmapFromURL(imageAPIResponse.url)
val filePath = imageSaver.saveBitmapToFile(bitmap, "image.jpg")
withContext(Main) {
databaseHelper.saveImageInRoom(imageAPIResponse, filePath)
}
}
if(apiSuccess){
val broadCastIntent = Intent()
broadCastIntent.action = "ACTION_FINISHED_SERVICE"
sendBroadcast(broadCastIntent)
}
} catch (exception: Exception) {
Log.d(TAG, "Exception occurred ${exception.message}")
}
}
}
companion object {
private const val JOB_ID = 2
#JvmStatic
fun enqueueWork(context: Context, intent: Intent) {
enqueueWork(context, MyService::class.java, JOB_ID, intent)
}
}
}
ServiceBroadcastReceiver.kt
class ServiceBroadcastReceiver : BroadcastReceiver() {
private val TAG = ServiceBroadcastReceiver::class.java.simpleName
private lateinit var _mNotificationManager: NotificationManager
private val _notificationId = 0
private val _primaryChannelId = "primary_notification_channel"
override fun onReceive(context: Context, intent: Intent) {
_mNotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
when (intent.action) {
"ACTION_FINISHED_SERVICE" -> {
deliverNotification(context)
}
}
}
private fun deliverNotification(context: Context) {
val contentIntent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context,_notificationId,contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context,_primaryChannelId)
builder.setSmallIcon(R.mipmap.ic_launcher)
builder.setContentTitle("Hi There")
builder.setContentText("Service finished its job")
builder.setContentIntent(pendingIntent)
builder.priority = NotificationCompat.PRIORITY_HIGH
builder.setAutoCancel(true)
builder.setDefaults(NotificationCompat.DEFAULT_ALL)
_mNotificationManager.notify(_notificationId,builder.build())
}
}
The getImageOfTheDay() is a suspend function inside WebService.kt
#Headers("Content-Type: application/json")
#GET("/v1/getImageOfTheDay")
suspend fun getImageOfTheDay(): Response<ImageAPIResponse>
If I move the code to outside the Coroutine scope, the broadcast is sent correctly.
How can I fix this problem ?
You should not use a coroutine here. The onHandleWork method is called on a background thread, and returning from this method signals the work is done and the service can be terminated.
As you are launching a coroutine with launch, the onHandleWork returns immediately and your service terminates.
You should call your network API directly and not in a coroutine because JobIntentService is designed to work this way already.

Categories

Resources