hannesa2 / paho.mqtt.android

Kotlin MQTT client for Android
432 stars 99 forks source link

toolbar overlapped with notfication bar #579

Closed fattyCoderHK closed 1 week ago

fattyCoderHK commented 8 months ago

For basicSample, the top toolbar is overlapped behind the Android top notification bar. Better add android:fitsSystemWindows="true" on activity_scrolling.xml to avoid this.

Also, I met with the case that Android Studio complain that functions such as registerForActivityResult being not known. But the app can be built and run correctly. I found that it is caused by different androidx.activity:activity are referenced. (https://stackoverflow.com/a/78086439/5290623)

I resolved by git clone LogcatCoreUI LogcatCoreLib project to upgrade them to use 1.8.0 to resolve. (Side note : After importing LogcatCoreLib/LogcatCoreUI, I need to add package="info.hannes.logcat" at AndroidManifest.xml and add the package to some resource reference at LogcatCoreUI to fix some unknown R resource issue.)

hannesa2 commented 8 months ago

For basicSample, the top toolbar is overlapped behind the Android top notification bar. Better add android:fitsSystemWindows="true" on activity_scrolling.xml to avoid this.

https://github.com/hannesa2/paho.mqtt.android/pull/580

The other issue is not clear to me what you mean exactly

fattyCoderHK commented 8 months ago

Below is the code I re-enable the notification for verification on basicSample. (Also, need to copy the ic_topic.png from extendedSample to really build the app.)

package info.mqtt.java.example

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import info.mqtt.android.service.MqttAndroidClient
import info.mqtt.android.service.QoS
import info.mqtt.java.example.databinding.ActivityScrollingBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.eclipse.paho.client.mqttv3.*
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.*

class MQTTExampleActivity : AppCompatActivity() {

    private lateinit var mqttAndroidClient: MqttAndroidClient
    private lateinit var adapter: HistoryAdapter
    private lateinit var binding: ActivityScrollingBinding
    private var hasNotificationPermissionGranted = false
    private val notificationPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            hasNotificationPermissionGranted = isGranted
            if (!isGranted) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if (Build.VERSION.SDK_INT >= 33) {
                        if (shouldShowRequestPermissionRationale(android.Manifest.permission.POST_NOTIFICATIONS)) {
                            showNotificationPermissionRationale()
                        } else {
                            showSettingDialog()
                        }
                    }
                }
            } else {
                Snackbar.make(findViewById(android.R.id.content), "notification permission granted", Snackbar.LENGTH_LONG).setAction("Action", null).show()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityScrollingBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        setSupportActionBar(binding.toolbar)

        if ((Build.VERSION.SDK_INT >= 33) && (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) !=
                    PackageManager.PERMISSION_GRANTED)) {
            notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
        } else {
            hasNotificationPermissionGranted = true
        }

        binding.fab.setOnClickListener { publishMessage() }
        val mLayoutManager: LayoutManager = LinearLayoutManager(this)
        binding.historyRecyclerView.layoutManager = mLayoutManager
        adapter = HistoryAdapter()
        binding.historyRecyclerView.adapter = adapter
        clientId += System.currentTimeMillis()

        //mqttAndroidClient = MqttAndroidClient(applicationContext, serverUri, clientId)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(CHANNEL_ID, "MQTT", NotificationManager.IMPORTANCE_LOW)
            channel.description = "MQTT Example Notification Channel"
            channel.enableLights(true)
            channel.lightColor = Color.GREEN
            channel.enableVibration(false)
            val notificationManager: NotificationManager = getSystemService(NotificationManager::class.java)
            notificationManager.createNotificationChannel(channel)

            val notificationBuilder: Notification.Builder =
                Notification.Builder(this, CHANNEL_ID)
                    .setContentTitle("MQTT Notification")
                    .setContentText("Content title")
                    .setSmallIcon(R.drawable.ic_topic)
            val foregroundNotification = notificationBuilder.build()
            mqttAndroidClient = MqttAndroidClient(applicationContext, serverUri, clientId).apply {
                setForegroundService(foregroundNotification)
            }
        } else {
            mqttAndroidClient = MqttAndroidClient(applicationContext, serverUri, clientId)
        }

        mqttAndroidClient.setCallback(object : MqttCallbackExtended {
            override fun connectComplete(reconnect: Boolean, serverURI: String) {
                if (reconnect) {
                    addToHistory("Reconnected: $serverURI")
                    // Because Clean Session is true, we need to re-subscribe
                    subscribeToTopic()
                } else {
                    addToHistory("Connected: $serverURI")
                }
            }

            override fun connectionLost(cause: Throwable?) {
                addToHistory("The Connection was lost.")
            }

            override fun messageArrived(topic: String, message: MqttMessage) {
                addToHistory("Incoming message: " + String(message.payload))
            }

            override fun deliveryComplete(token: IMqttDeliveryToken) {}
        })
        val mqttConnectOptions = MqttConnectOptions()
        mqttConnectOptions.isAutomaticReconnect = true
        mqttConnectOptions.isCleanSession = false
        addToHistory("Connecting: $serverUri")
        mqttAndroidClient.connect(mqttConnectOptions, null, object : IMqttActionListener {
            override fun onSuccess(asyncActionToken: IMqttToken) {
                val disconnectedBufferOptions = DisconnectedBufferOptions()
                disconnectedBufferOptions.isBufferEnabled = true
                disconnectedBufferOptions.bufferSize = 100
                disconnectedBufferOptions.isPersistBuffer = false
                disconnectedBufferOptions.isDeleteOldestMessages = false
                mqttAndroidClient.setBufferOpts(disconnectedBufferOptions)
                subscribeToTopic()
            }

            override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                addToHistory("Failed to connect: $serverUri")
            }
        })
    }

    override fun onDestroy() {
        Timber.d("onDestroy")
        mqttAndroidClient.disconnect()
        super.onDestroy()
    }

    private fun addToHistory(mainText: String) {
        Timber.d(mainText)
        @SuppressLint("SimpleDateFormat")
        val timestamp = SimpleDateFormat("HH:mm.ss.SSS").format(Date(System.currentTimeMillis()))
        CoroutineScope(Dispatchers.Main).launch {
            adapter.add("$timestamp $mainText")
        }
        Snackbar.make(findViewById(android.R.id.content), mainText, Snackbar.LENGTH_LONG).setAction("Action", null).show()
    }

    fun subscribeToTopic() {
        mqttAndroidClient.subscribe(subscriptionTopic, QoS.AtMostOnce.value, null, object : IMqttActionListener {
            override fun onSuccess(asyncActionToken: IMqttToken) {
                addToHistory("Subscribed! $subscriptionTopic")
            }

            override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
                addToHistory("Failed to subscribe $exception")
            }
        })

        // THIS DOES NOT WORK!
        mqttAndroidClient.subscribe(subscriptionTopic, QoS.AtMostOnce.value) { topic, message ->
            Timber.d("Message arrived $topic : ${String(message.payload)}")
            addToHistory("Message arrived $message")
        }
    }

    private fun publishMessage() {
        val message = MqttMessage()
        message.payload = publishMessage.toByteArray()
        if (mqttAndroidClient.isConnected) {
            mqttAndroidClient.publish(publishTopic, message)
            addToHistory("Message Published >$publishMessage<")
            if (!mqttAndroidClient.isConnected) {
                addToHistory(mqttAndroidClient.bufferedMessageCount.toString() + " messages in buffer.")
            }
        } else {
            Snackbar.make(findViewById(android.R.id.content), "Not connected", Snackbar.LENGTH_SHORT).setAction("Action", null).show()
        }
    }

    private fun showSettingDialog() {
        MaterialAlertDialogBuilder(this, com.google.android.material.R.style.MaterialAlertDialog_Material3)
            .setTitle("Notification Permission")
            .setMessage("Notification permission is required, Please allow notification permission from setting")
            .setPositiveButton("Ok") { _, _ ->
                val intent = Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
                intent.data = Uri.parse("package:$packageName")
                startActivity(intent)
            }
            .setNegativeButton("Cancel", null)
            .show()
    }

    private fun showNotificationPermissionRationale() {

        MaterialAlertDialogBuilder(this, com.google.android.material.R.style.MaterialAlertDialog_Material3)
            .setTitle("Alert")
            .setMessage("Notification permission is required, to show notification")
            .setPositiveButton("Ok") { _, _ ->
                if (Build.VERSION.SDK_INT >= 33) {
                    notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
                }
            }
            .setNegativeButton("Cancel", null)
            .show()
    }

    companion object {
        private const val serverUri = "tcp://broker.hivemq.com:1883"
        private const val subscriptionTopic = "exampleAndroidTopic"
        private const val publishTopic = "exampleAndroidPublishTopic"
        private const val publishMessage = "Hello World"
        private var clientId = "BasicSample"
        private const val CHANNEL_ID = "BasicSampleNotificationChannelID"
    }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Permissions the Application Requires -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <application
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:name="info.hannes.logcat.LoggingApplication"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">
        <activity
            android:name="info.mqtt.java.example.MQTTExampleActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
            android:name="info.mqtt.android.service.MqttService"
            android:foregroundServiceType="dataSync" />

    </application>

</manifest>

The app can be built and run. However, Android Studio will complain about the "registerForActivityResult" with unresolved reference. Android Studio that I have tested with.

Android Studio Iguana | 2023.2.1 Build #AI-232.10227.8.2321.11479570, built on February 22, 2024 Runtime version: 17.0.9+0--11185874 amd64 VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o. Windows 10.0 GC: G1 Young Generation, G1 Old Generation Memory: 3048M Cores: 8 Registry: ide.experimental.ui=true

image

My solution is posted on Stackoverflow but may not really detail enough there (https://stackoverflow.com/a/78086439/5290623). It is not a big deal as the code can still run, it is just a bit itching for my taste.

hannesa2 commented 8 months ago

@fattyCoderHK Maybe a pull request with the solution make sense. Normally I don't assemble code fragments to recap the issue. But I did it exceptionally. Btw, R.drawable.ic_topic is missing.

Anyway, you promoted solution from stackoverflow is not working. With a pull request, you have the chance to show a working solution

hannesa2 commented 1 week ago

related to https://github.com/hannesa2/paho.mqtt.android/pull/580, https://github.com/hannesa2/paho.mqtt.android/pull/708 and https://github.com/hannesa2/paho.mqtt.android/pull/707