Closed fattyCoderHK closed 1 week 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
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
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.
@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
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.)