Eventviewer
Der Eventviewer zeigt Ereignisse in einem Micro-Service Umgebung aller Programme die Ereignisse an den Eventlogger senden. Die HOBRO (Home-Applikation Brotbeck) verwendet einen RabbitMQ Messagebus mit AMPQ und MQTT Protokoll. Der Android Eventviewer kommuniziert mit dem Eventlogger via MQTT V3.

Facts:
- Android Studio Koala
Verision 2024.1.2 Patch 1 - Kotlin V 2.0
- Connected to RabbitMQ
via MQTT V3 - SQLite mit Room
- Recycler View
- Language De, En, Es
- Preferences.xml
Motivation
- Einfacher Eventviewer welcher die Events anzeigt beim Entwickeln und Testen
- Lernen von Android Studio mit Kotlin
- Vorbereitung für die Android HOBRO App
- Lernen von Android Studio Koala.
Herausforderung:
- IDE
- Komponenten
- Dokumentation
- Debugging
- Version Konflikte
- Emulatoren
- MQ Event, Viewmodel und DbModel Konvertierung
- UTC Time Strings
Funktionsbeschreibung
Das Programm Eventviewer war für mich in erster Linie ein Programm zum lernen von Kotlin und Android Studio Koala.
Wenn der Eventviewer gestartet wird, dann verbindet er sich mit dem Messagebus und erhält die Events die der Eventlogger von den laufenden Programmen empfängt..
- Sobald der der Eventviewer eine Verbindung zum Messagebus aufgebaut hat, sendet er ein Event an den Eventlogger. Der wiederum das Event speichert und an die Eventviewer weiterleitet, Sobald der Eventviewer sein Event erhält ist alles in Butter,
- Die Events haben einen Schweregrad (severity) nach denen man filtern kann (Debug, Info,, Warning,, Error und Fatal) .
- Die Events haben eine Nummer, idR, sind es tausender Nummer., Ich führe eine Nummertabelle damit kein Eventnummer doppelt verwendet wird. Laufen verschiedene Programme im System, so kann man damit sehr schnell herausfinden wer das Event ausgelöst hat.
- Da nicht alle Informationen in der Liste ersichtlich sind, kann man durch klicken auf das Event das Detailfragment anzeigen. Klickt man auf ein Infofeld, dann werden ein paar Sekunden imunteren Bereich Erklärungen angezeigt.
- Klickt man auf FAB (i) dann wird kurz die Anzahl Events die in der Liste ausgewählt sind angezeigt.
- Im Settings kann man die Messagbus Konfiguration festlegen, URlL des Brokers und die Zugriffsberechtigung des Anwenders. Danach muss die App neu gestartet werden.
- Es gibt eine Abaut-Seite, Settings-Seite und Filter-Seite, letzteres hat noch keine Verwendung.
- Die Events werden in der lokalen DB SQLite gespeichert. Mit drei mal klicken auf Fab innerhalb 2 Sekunden werden ältere Events gelöscht. Im Setting kan man die Anzahl tage die behalten werden sollen festlegen (Neustart der App).
- Je nach eingestellter Android Sprache ist das GUI in Deutsch, Englisch oder Spanisch.

Entwicklungsumgebung

Android Studio Koala
Das A-Studio ist sehr umfangreich. Es gibt ein neues und altes GUI. Da ich schon früher einmal mit Android gearbeitet habe, dachte ich im alten GUI werde ich mich besser zurecht finden., dem war aber nicht so. Ich dachte ich mache ein Master/Detail Activity. Früher gab es eine solche Vorlage. Im AS nicht mehr.
- Downloaden und installieren von Android Studio 2024
- Zuerst sollte man alles auf Update prüfen und installieren.
- Die benötigten SDK auswählen und installieren.
- Dann ein neues Projekt erstellen mit einem Template und bilden.
- Auf einem Emulator laufen lassen, um zu sehen ob alles funktioniert.
Emulatoren:
Da ich mit Visual Studio 2022 bereits Anwendungen mit Xamarin und MAUI programmiert habe, habe ich auch Android Emulatoren im VS verwendet. Diese Emulatoren sind am gleichen Ort verwaltet wie diese vom AS und es werden auch die gleichen Umgebungsvariablen verwendet. Daher funktionierten meine Emulatoren überhaupt nicht. Ich musste alle weglöschen und neu definieren. Es gibt ein Log unter dem persönliche „Appdata local google android Studio“ Ordner wo man Hinweise auf die Emulatoren Probleme erhält. Nebst den virtuellen Emulatoren verwendete ich noch 2 physikalische Geräte, ein HTC 11 Plus und ein Samsung A53. Das HTC gab die minimale Target Api Version von 24 vor.
Komponenten
Wie bei allen modernen Entwicklungsumgebungen arbeitet man mit Komponenten die einem das Arbeiten erleichtern soll. Dabei ist auch hier für den Einsteiger das nackte Chaos vorprogrammiert. Man kann es so ausdrücken:
- Man braucht etwa eine Tag bis man die richtigen Komponenten mit den richtigen Versionen die man benötigt zusammen hat.
- Dann ca. 1 Std um die Komponenten zusammen zu kleben, schrauben, gradbiegen etc.
- Und ca. 10 Min für den eigenen Code zu implementieren.
Das ganze Ökosystem von Android besteht aus unendlich vielen Komponenten die man entsprechend zusammenbaut, so wie Lego. Es ist eine Kunst die richten zusammenpassende Elemente zu finden und zu verwenden. Eigener Code ist eigentlich Nebensache.
Explizit von mir verwendete Komponenten:
- androidx-room
- androidx-preference
- github-paho-mqtt-android (hannesa2)
- androidx-lifecycle-livedata
- androidx-sqlite
- androidx-recyclerview
- androidx-lifecycle-viewmodel
Programmbeschreibung

MQTT
Ich habe mich für MQTT entschieden, weil dieses Protokoll bei vielen IoT ein Thema ist. Ich hatte dann Probleme mit den Sicherheits Themen von Android, so dass ich auf die Komponente Hannesa2 ausweichen musste.
Für den Broker habe ich RabbitMQ gewählt. Erstens läuft er bereits auf meinem Rechner. Zweitens habe ich ein grosses Projekt in C# damit umgesetzt und drittens ist er aus meiner Erfahrung sehr effizient und stabil. Und wie ich gesehen habe, hat er neuerdings Streams-Datastruktur, die eventuell bei Videoüberwachung ums Haus gute Dienste leisten könnte.
Den RabbitMQ ist gut Dokumentiert und mit etwas Engagement lässt er sich gut installieren. Bei mir läuft er auf Windows11, aber später will ich ihn auf einem Rasperry Pi installieren. Damit man mit RabbitMQ MQTT benutzen kann muss man das MQTT Plugin installieren und konfigurieren. Es gibt ein paar Eigenschaften die man beachten muss, ich habe für mein Projekt ein virtueller Host definiert mit dem Namen HOBRO-01. In meinem Projekt arbeite ich hauptsächlich mit A;MQP. Der RabbitMQ übernimmt die Konvertierung von MQTT nach AMQP und umgekehrt. Der Eventlogger sendet die Events an den MQTT-Exchange mit dem Topic „eventlog.satellit“ das dann im MQTT als „eventlog/satellit“ gewandelt wird. Die Eventviewer ’subsrciben ‚ an das Topic „eventlog“. Das „satellit braucht es für den Eventlogger, damit nicht eine endlos Schleife entsteht. die Client können dies auslassen. Eine Eigenschaft von MQTT Topic mit hierarchischen Strukturen.
/**
* Extract from the MqttHelper.kt module. This is build with help from
* https://medium.com/swlh/android-and-mqtt-a-simple-guide-cb0cbba1931c
* Android and MQTT: A Simple Guide by Leonardo Cavagnis
**/
fun connect(username: String = "",
password: String = "",
cbConnect: IMqttActionListener = defaultCbConnect,
cbClient: MqttCallback = defaultCbClient) {
mqttClient?.setCallback(cbClient)
val options = MqttConnectOptions()
options.isAutomaticReconnect = true
options.connectionTimeout = 10
options.isCleanSession = false
options.userName = username
options.password = password.toCharArray()
try {
mqttClient?.connect(options, null, cbConnect)
} catch (e: MqttException) {
e.printStackTrace()
}
}
/* some part on create view in FragmentEventViewer */
//region MQTT Client
MqttHelper(requireContext(), Globi.PMQTT_SERVER_URI, Globi.PMQTT_CLIENT_ID).also { mqtt = it }
if (mqtt.isConnected()!!) {
tmp = myActivity.getString(R.string.common002)
Log.d(this.javaClass.name, tmp)
Toast.makeText(requireContext(),tmp, Toast.LENGTH_LONG).show()
} else {
try {
mqtt.connect(Globi.PMQTT_USERNAME,Globi.PMQTT_PWD, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
val msg = myActivity.getString(R.string.common003)
Log.d(this.javaClass.name, msg)
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
} else {
// Looper.prepare() // this is maybe a problem looper
Handler(Looper.getMainLooper()).post {
Toast.makeText(requireContext(),msg, Toast.LENGTH_LONG).show()
}
}
subscribeToTopic(Globi.XMQTT_EV_RECEIVE_TOPIC)
tmp = myActivity.getString(R.string.brokerName)
msg1 = myActivity.getString(R.string.common001,Globi.PMQTT_CLIENT_ID,Globi.XMQTT_EV_RECEIVE_TOPIC,tmp)
// This Message goes to the server and then looped back to all satellites. If we receive this message then all is okay!
logEventToEventServer(3101,'I',msg1)
}
override fun onFailure( asyncActionToken: IMqttToken?, exception: Throwable? ) {
// in case of failure it makes no sense to send a message to the Server
val txt = myActivity.getString(R.string.error001)
val msg = txt + exception.toString()
saveEventInRepository(msg)
Log.d(this.javaClass.name,msg )
if (Looper.myLooper() == null) Looper.prepare()
if (Looper.getMainLooper().thread == Thread.currentThread()) {
Toast.makeText(requireContext(),msg, Toast.LENGTH_SHORT ).show()
} else {
Handler(Looper.getMainLooper()).post {
Toast.makeText(requireContext(),msg, Toast.LENGTH_LONG).show()
}
}
}
},
object : MqttCallback {
override fun messageArrived(topic: String?, message: MqttMessage?) {
// val msg = "Receive message: ${message.toString()} from topic: $topic"
try {
msg1 = message.toString()
val item = eventlogViewModel.processMessage(msg1)
eventlogViewModel.add(item)
if (Looper.myLooper() == null) Looper.prepare()
Handler(Looper.getMainLooper()).post {
adapter.notifyDataSetChanged()
val cnt = adapter.itemCount
recyclerView.smoothScrollToPosition(cnt)
}
}catch (e: Exception) {
val ex = e.message
}
}
override fun connectionLost(cause: Throwable?) {
tmp = myActivity.getString(R.string.common004)
Log.d(this.javaClass.name, tmp + " " + cause.toString() )
}
override fun deliveryComplete(token: IMqttDeliveryToken?) =Unit
}
) // end MQTT Connect
} catch (e : Exception){
tmp = myActivity.getString(R.string.error002)
Log.d(this.javaClass.name, tmp + e.message)
}
}
return root
}
//endregion
Listview (Recyclerview)
Ich verwende ein Viewmodel mit Viewbinding zwischen Layout und Viewmodel. Damit das funktioniert muss im Modul Gradl Viewbinding auf true gesetzt werden. Dadurch werden die Views-ID im Code als Variablen verfügbar. Dabei werden eventuelle Underscore aus der ID für die Variablen nicht übernommen, sowie die Camelcase Notaion angewendet. (Event_Nr => eventNr)
class EventlogAdapter(mainFragment: Fragment, viewModel: EventlogViewModel) : ListAdapter<EventlogViewModel.VmEvent, EventlogViewHolder>(object : DiffUtil.ItemCallback<EventlogViewModel.VmEvent>() {
override fun areItemsTheSame( oldItem: EventlogViewModel.VmEvent, newItem: EventlogViewModel.VmEvent): Boolean = oldItem == newItem
override fun areContentsTheSame(oldItem: EventlogViewModel.VmEvent, newItem: EventlogViewModel.VmEvent): Boolean = oldItem == newItem
}) {
private var mf = mainFragment
private var vm = viewModel
private val severityImg = mapOf( 'D' to R.drawable.sev_debug, 'I' to R.drawable.sev_info,
'W' to R.drawable.sev_warning, 'E' to R.drawable.sev_error,
'F' to R.drawable.sev_fatal, '?' to R.drawable.sev_other)
/**
* On create view holder
* it is important that you pass the parent for match_parent is working correct in layout
* @param parent
* @param viewType
* @return
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventlogViewHolder {
val binding = ItemEventviewBinding.inflate(LayoutInflater.from(parent.context),parent, false,)
return EventlogViewHolder(binding)
}
/**
* On bind view holder
* für each element in the list we load the data to the viewholder. Here we
* can also make the strips dark and light if odd or even of position.
* @param holder
* @param position
*/
override fun onBindViewHolder(holder: EventlogViewHolder, position: Int) {
holder.viewEvNr?.text = String.format("%04d", getItem(position).eventNr)
holder.viewEvFacility?.text = getItem(position).facility
holder.viewEvProgram?.text = getItem(position).program
holder.viewEvTime?.text = getItem(position).timeStamp
holder.viewEvImage?.setImageDrawable(severityImg[getItem(position).severity]?.let {
holder.viewEvImage?.let { it1 -> ResourcesCompat.getDrawable( it1.resources, it, null ) }
})
holder.viewEvMessage?.text = getItem(position).message
val ctx = holder.viewEvImage?.context
if (position % 2 == 1) holder.viewRoot?.setBackgroundColor(getColor(ctx!!,R.color.stripLight))
else holder.viewRoot?.setBackgroundColor(getColor(ctx!!,R.color.stripDark))
/**
* Every item gets a clicklistener
* if we click on an item then we will show the details about this event. For that we
* load or display the detail fragment and hide the main fragment. before we load the
* viewmodel property currentItem with the details and show the home icon in the
* toolbar for going back. that is all we have to do.
*/
holder.itemView.setOnClickListener() {
vm.currentItem = getItem(position)
val fm = mf.parentFragmentManager
val frM = fm.findFragmentByTag("frMain")
var frD = fm.findFragmentByTag("frDetail")
val transaction = fm.beginTransaction()
if (frD == null) {
transaction.add(R.id.fragment_container_view, EventDetailFragment(),"frDetail")
transaction.setReorderingAllowed(true)
transaction.addToBackStack("detail")
frD = EventDetailFragment()
}
frM ?.let { transaction.hide(frM) }
transaction.show(frD).commit()
vm.goHomeIcon = true
mf.activity?.invalidateOptionsMenu()
}
}
}
/**
* Eventlog view holder
* Remember! the names of binding.variables are generated from layout view
* if viewmodel building is enabled.
* @constructor
*
* @param binding
*/
class EventlogViewHolder(binding: ItemEventviewBinding) : RecyclerView.ViewHolder(binding.root) {
val viewRoot: LinearLayout? = binding.listEventItem
val viewEvImage: ImageView? = binding.imgEvSeverity
val viewEvNr: TextView? = binding.txtEvNr
val viewEvFacility: TextView? = binding.txtEvFacility
val viewEvMessage: TextView? = binding.txtEvMessage
val viewEvProgram: TextView? = binding.txtEvProgram
val viewEvTime: TextView? = binding.txtEvTime
}
}
DB-Repository
Ich verwende ein Viemodel mit Viewbinding zwischen Layout und Viemodel. Damit da funktionert muss im Modul Gradl viewbinding auf true gesetkzt werden. Dadurch werden die Views-ID im Code als Variablen verfügbar. Dabei werden eventuelle Underscore aus der ID für die Variablen nicht übernommen, sowie die Camelcase Notaion angewendet. (Event_Nr => eventNr)
package com.brotbeck.droidcockpit.model
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
/**
* Data access layer
* generates the object DAO as singleton for accessing the SQLite DB.
*
* @constructor Create empty Dal
*/
@Database(entities = [sqlEvent::class], version = 2, exportSchema = false)
abstract class DAL : RoomDatabase() {
abstract fun eventDao(): EventDao
companion object {
@Volatile
private var INSTANCE: DAL? = null
fun getDatabase(context: Context): DAL {
return INSTANCE ?: synchronized(DAL::class) {
val instance = Room.databaseBuilder(
context.applicationContext, DAL::class.java, "EventlogDB.db"
).build()
INSTANCE = instance
instance
}
}
}
}
package com.brotbeck.droidcockpit.model
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
/**
* Event dao
* There is not many things to explain. The queries are plain SQL Statement and the
* function below the SQLStatement is mapped to the query.
*
* Remember! you need only define an interface, the precompiler shall then
* create the java implementation depending of the defined annotation. See folder 'Java (generated)'
*
* @constructor Create empty Event dao
*/
@Dao
interface EventDao {
@Insert
suspend fun insert(ev: sqlEvent)
@Delete
suspend fun delete(ev: sqlEvent)
@Query("DELETE FROM Eventlog")
suspend fun clear()
@Query("DELETE FROM Eventlog WHERE timeStamp < :dat")
suspend fun removeBefore(dat: Long)
@Query("SELECT * FROM Eventlog" )
suspend fun getAll():List<sqlEvent>
@Query("SELECT COUNT(*) FROM Eventlog")
suspend fun count():Int
@Query("SELECT COUNT(*) FROM Eventlog WHERE severity >= :sev ")
suspend fun filteredCount(sev: Int) :Int
@Query("SELECT * FROM Eventlog WHERE severity >= :sev " )
suspend fun getAllFiltered(sev: Int): List<sqlEvent>
@Query("SELECT MAX(id) FROM Eventlog")
fun getMaxPk():Long
}
Auszug EventlogDBRepository
package com.brotbeck.droidcockpit.model
import android.content.Context
import androidx.core.math.MathUtils.clamp
import com.brotbeck.droidcockpit.helper.HelperLib
import com.brotbeck.droidcockpit.ui.eventlog.EventlogViewModel
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
import kotlin.random.Random
//!! The data access must be not in UI thread and not in main thread for that it is
//!! asynchronous. But I am not familiar with the coroutine concept. It was a little
//!! try and error for me:-)
/**
* Eventlog db repository
* this repository is creating and maintaining a SQLite db as Repository. It
* implements an interface. the Annotation is used to generate java code by
* precompiler. if the library are not correct configured then no code will
* be generated. You can see it in subfolder java (generated)
*
* @constructor
*
* @param ctx
*/
class EventlogDBRepository(ctx: Context): IRepository {
/**
* Companion
* we need a singleton for the local filter of data. The property actualFilter
* must be for allover the same field.
* @constructor Create empty Companion
*/
companion object {
@Volatile
private var INSTANCE: EventlogDBRepository? = null
fun getInstance(context: Context) =
INSTANCE ?: synchronized(this) {
INSTANCE ?: EventlogDBRepository(context).also { INSTANCE = it }
}
}
/**
* You can filter the events by the ordinal value of severity. If it is 0
* then all events are returned. if filter is 1 the debug messages are filtered out
* and only information and up are returned and so on.
* override var actualFilter = 0;
**/
private val db = DAL.getDatabase(ctx)
override var actualFilter = 0
override fun changeFilter(i: Int){
actualFilter = clamp(i,0,4);
}
override fun count(): Int {
var cnt = 0
GlobalScope.launch {
cnt = db.eventDao().count()
}
return cnt
}
/**
* Filtered count
* the app can set the filter from which severity the data should be fetched. it is then
* fixed until the app does it change again.
* @return
*/
override fun filteredCount() : Int {
var cnt = 0
runBlocking {
cnt = db.eventDao().filteredCount(actualFilter)
}
return cnt
}
Preferences (Settings)
Ich verwende eine XML Datei um die Settings in den den shared Preferencen abzuspeichern. Das ist sehr einfach, aber nicht so flexibel für eigene Gestaltung.
Im Settingsfragement muss man nur das XML laden und schon werden die Felder gespeichert.
Mittels Listener kann man einfach Validierungen Eigenschaften umsetzen.
- Password Felder
- Regex
- Länge
preferences.xml
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:summary="@string/prefSummary">
<!--below line is to create preference category-->
<PreferenceCategory android:title="MQTT-Settings"
android:summary="@string/prefCatSummaryMQTT">
<EditTextPreference
android:key="@string/prefKeyUrl"
android:title="@string/prefTitleUrl"
android:summary="@string/prefSummaryUrl"
android:inputType="textUri"/>
<EditTextPreference
android:key="@string/prefKeyUser"
android:title="@string/prefTitleUser"
android:summary="@string/prefSummaryUser"
android:inputType="text"/>
<EditTextPreference
android:key="@string/prefKeyPw"
android:title="@string/prefTitlePw"
android:summary="@string/prefSummaryPw"
android:inputType="textPassword"/>
<EditTextPreference
android:key="@string/prefKeyCid"
android:title="@string/prefTitleCid"
android:summary="@string/prefSummaryCid"
android:inputType="text"/>
</PreferenceCategory>
<PreferenceCategory android:title="DB-Settings">
<EditTextPreference
android:key="@string/prefKeyDB"
android:title="@string/prefTitleDB"
android:summary="@string/prefSummaryDB"
android:defaultValue="1"
android:inputType="number"/>
</PreferenceCategory>
</PreferenceScreen>
Settings Fragment
package com.brotbeck.droidcockpit.ui.settings
import android.content.Context
import android.os.Bundle
import android.text.InputType
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceFragmentCompat
import com.brotbeck.droidcockpit.R
fun Context.toast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
/**
* Settings fragment
* is a standard implementation of a setting screen and is using shared preferences
* as storage of preferenece values.
*
* @constructor Create empty Settings fragment
*/
class SettingsFragment : PreferenceFragmentCompat() {
/**
* On create preferences
* loads the setting fragment with the file preferences.xml and sets some
* changeListener (url,Cid). All strings are in strings.xml declared
* @param savedInstanceState
* @param rootKey
*/
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
var prefKey: CharSequence = ""
setPreferencesFromResource(R.xml.preferences, rootKey);
prefKey = context?.getString(R.string.prefKeyPw) as CharSequence
val passwordPreference: EditTextPreference? = findPreference(prefKey)
// following is used to hide the password letter when editing
passwordPreference?.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
prefKey = context?.getString(R.string.prefKeyUrl) as CharSequence
findPreference<EditTextPreference>(prefKey)?.setOnPreferenceChangeListener { _, newValue ->
val pattern = "^(tcp:)\\/\\/[\\w,\\.]+(:(\\d)+)\$" // is only a rudimentary check of url
val valid = Regex(pattern, RegexOption.IGNORE_CASE).matches(newValue as String)
if (!valid ) {
val msg = context?.getString(R.string.prefInvalidUrl) as CharSequence
context?.toast(msg)
}
valid // if true the value is stored in shared preferences
}
prefKey = context?.getString(R.string.prefKeyCid) as CharSequence
findPreference<EditTextPreference>(prefKey)?.setOnPreferenceChangeListener { _, newValue ->
val valid = newValue.toString().length > 4
if (!valid ) {
val msg = context?.getString(R.string.prefInvalidCid) as CharSequence
context?.toast(msg)
}
valid
}
}
}
Globi sind globale Variablen und werden zum Teil mit Preferenzen überschrieben. Das ist hier eine einfache Lösung, denn wenn die Präferenzen im Setting angepasst werden muss die App neu gestartet werden.
globi.kt
/**
* Get shared preference
* sets the Values in the globi by the preferences from Settings Fragemt.
* The Settings value ar only readed by startup the app.
*/
private fun loadGlobiWithSharedPreference(){
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this /* Activity context */)
sharedPreferences.getString("MQTTBrokerUrl", "")?.let { Globi.PMQTT_SERVER_URI = it } // if fun return is not null then set globi
sharedPreferences.getString("MQTTUserName", "")?.let { Globi.PMQTT_USERNAME = it }
sharedPreferences.getString("MQTTPassword", "")?.let { Globi.PMQTT_PWD = it }
sharedPreferences.getString("MQTTClientId", "")?.let { Globi.PMQTT_CLIENT_ID = it }
}
Goodies
Goodies
Challenges
- Debugger:
Der Debugger ist irgendwie falsch konfiguriert. Wenn man ein Breakpoint auf eine Expression setzt die nicht simple evaluiert werden kann, dann hängt er für eine Ewigkeit.
Intro Android Studio
SQLitte exploring in AS-Koala:
Mit dem AppInspection kann man die Tabellen und Daten der DB auf dem Device ansehen und verändern. AppInspection findet man mit:
Help> find action >
Libraries install:
wenn man den src Ordnerausgewählt hat, kann man unter:
build > edit library
Komponenten hinzufügen aktualisieren, entfernen. Sie werden in der Datei unter gradle > libs.versions.toml aktualisiert.