Creating a Laravel Logger - Part 4: Creating our Android application
For this part of the series, you will need Android Studio 3+ installed on your machine.
In this part, we will build an Android application for our logger. The Android app will display logs in a list and receive notifications for errors. We will combine the functionalities of Pusher Channels and Pusher Beams to achieve this.
In the previous parts of this series, we have been able to create the Laravel application that will push all the logs to Pusher. We also added the option to push the logs to Beams which will be triggered only when the log level is critical (error).
Here is how your app will look:
Let’s dig in!
Requirements
To follow along with this series you need the following things:
- Completed previous parts of the series. Part 1, Part 2, Part 3
- Laravel installed on your local machine. Installation guide.
- Knowledge of PHP and the Laravel framework.
- Composer installed on your local machine. Installation guide.
- Android Studio >= 3.x installed on your machine (If you are building for Android).
- Knowledge of Kotlin and the Android Studio IDE.
- Xcode >= 10.x installed on your machine (If you are building for iOS).
- Knowledge of the Swift programming language and the Xcode IDE.
- A Pusher application. Create one here.
- A Pusher Beams application. Create one here.
Creating the project
Open Android Studio and create a new application. Enter the name of your application, for example, AndroidLoggerClient
and enter a corresponding package name. You can use com.example.androidloggerclient
for your package name.
Make sure the Enable Kotlin Support check box is selected as this article is written in Kotlin. Next, select a suitable minimum SDK for your app, API 19 should be fine. Next, choose the Empty Activity template provided, stick with the MainActivity
naming and click Finish. You may have to wait a while Gradle will prepare your project.
Completing Pusher Beams setup
Since Pusher Beams for Android relies on Firebase, we need an FCM key and a google-services.json
file for our project. Go to your Firebase console and click the Add project card to initialize the app creation wizard.
Add the name of the project, read and accept the terms and conditions. After this, you will be directed to the project overview screen. Choose the Add Firebase to your Android app option. Enter the app’s package name - com.example.androidloggerclient
(in our case), thereafter you download the google-services.json
file. After downloading the file, skip the rest of the quick-start guide.
Add the downloaded file to the app folder of your project - AndroidLoggerClient/app/
.
To get the FCM key, go to your project settings on Firebase, under the Cloud Messaging tab, copy out the server key.
Open the Pusher Beams instance created earlier in the series, start the Android quick start and enter your FCM key. After adding it, select Continue and exit the guide.
Adding app dependencies
Here, we will add dependencies to be used for the application. First, open your project build.gradle
file and add the google services classpath like so:
// File: ./build.gradle
// [...]
dependencies {
// other claspaths
classpath 'com.google.gms:google-services:4.2.0'
}
// [...]
Next, you open the main app build.gradle
file and add the following:
// File: ./app/build.gradle
// [...]
dependencies {
// other dependencies
implementation 'com.pusher:pusher-java-client:1.8.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:cardview-v7:28.0.0'
implementation 'com.google.firebase:firebase-messaging:17.3.4'
implementation 'com.pusher:push-notifications-android:0.10.0'
}
apply plugin: 'com.google.gms.google-services'
// [...]
This snippet adds Pusher’s dependencies for the app. We equally have some dependencies from the Android support library to help us in building our UIs. Next, sync your Gradle files.
Implementing realtime logs
We will now implement realtime logs for the app. These logs will be displayed on a list, so let’s start by setting up our list. Open the activity_main.xml
file and replace it with this:
<!-- File: ./app/src/main/res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.androidloggerclient.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
This file represents the main screen of the app. Here we added a recyclerview
, which represents the UI element for lists. We will configure it as we proceed. The next thing we will do is design how each item will look like. Create a new layout file log_list_row.xml
and paste this:
<!-- File: ./app/src/main/res/layout/log_list_row.xml -->
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:cardCornerRadius="5dp"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_margin="10dp"
android:layout_height="match_parent">
<TextView
android:id="@+id/logMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Hello Logger!"
/>
<TextView
android:id="@+id/logLevel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/logMessage"
tools:text="Warning"
android:textSize="12sp"
/>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
This layout contains a cardview
that wraps two texts. One text is for the log message and the other for the log level. We will now create a corresponding data model class which will hold two strings.
Create a new class named LogModel
and paste this:
// File: ./app/src/main/java/com/example/androidloggerclient/LogModel.kt
data class LogModel(val logMessage:String , val logLevel:String)
Next, we need a class to manage items in the list, also called an adapter. Create a new class named LoggerAdapter
and paste this:
// File: ./app/src/main/java/com/example/androidloggerclient/LoggerAdapter.kt
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
class LoggerAdapter : RecyclerView.Adapter<LoggerAdapter.ViewHolder>() {
private var logList = ArrayList<LogModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.log_list_row, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(logList[position])
override fun getItemCount(): Int = logList.size
fun addItem(model: LogModel) {
this.logList.add(model)
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val logMessage = itemView.findViewById<TextView>(R.id.logMessage)!!
private val logLevel = itemView.findViewById<TextView>(R.id.logLevel)!!
fun bind(item: LogModel) = with(itemView) {
logMessage.text = item.logMessage
logLevel.text = item.logLevel
when {
item.logLevel.toLowerCase() == "warning" -> {
logLevel.setTextColor(ContextCompat.getColor(context, R.color.yellow))
}
item.logLevel.toLowerCase() == "error" -> {
logLevel.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
}
item.logLevel.toLowerCase() == "info" -> {
logLevel.setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_light))
}
}
}
}
}
The adapter manages the list through its implemented methods marked with override
. The onCreateViewHolder
method uses our log_list_row
layout to inflate each row of the list using a custom ViewHolder
class created at the bottom of the snippet. The onBindViewHolder
binds data to each item on the list, the getItemCount
method returns the size of the list. The addItem
method adds data to the list and refreshes it.
Also, in the above snippet, we add color to log level text based on the type of log. We imported the yellow into our colors.xml
file, so add the color in your colors.xml
file like so:
<!-- File: ./app/src/main/res/values/colors.xml -->
<color name="yellow">#FFFF00</color>
To finish the first part of our implementation, open your MainActivity.Kt
file and do the following:
Add the following imports:
// File: ./app/src/main/java/com/example/androidloggerclient/MainActivity.kt
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager
import com.pusher.client.Pusher
import com.pusher.client.PusherOptions
import kotlinx.android.synthetic.main.activity_main.*
import org.json.JSONObject
This imports external classes we will make use of. Then you initialize the adapter in the class like so:
// File: ./app/src/main/java/com/example/androidloggerclient/MainActivity.kt
// [...]
class MainActivity : AppCompatActivity() {
private val mAdapter = LoggerAdapter()
// [...]
}
Next, you replace the onCreate
method with this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupRecyclerView()
setupPusher()
}
This method is one of the lifecycle methods in Android. Here, we called two other methods to help set up the recyclerview
and Pusher. Add the methods like so:
private fun setupRecyclerView() {
with(recyclerView){
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = mAdapter
}
}
This assigns a layout manager and our initialized adapter instance to the recyclerview
.
private fun setupPusher() {
val options = PusherOptions()
options.setCluster("PUSHER_CLUSTER")
val pusher = Pusher("PUSHER_API_KEY", options)
val channel = pusher.subscribe("log-channel")
channel.bind("log-event") { channelName, eventName, data ->
println(data)
val jsonObject = JSONObject(data)
val model = LogModel(jsonObject.getString("message"), jsonObject.getString("loglevel"))
runOnUiThread {
mAdapter.addItem(model)
}
}
pusher.connect()
}
This sets up Pusher to receive logs from a Pusher channel.
Replace the Pusher placeholders with your own keys from your dashboard.
Finally, add the internet permission to the AndroidManifest.xml
file like so:
<!-- File: ./app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET"/>
With this, whenever we receive a log, it is added to the list through the adapter. With this, the app can display logs as soon as events come in. Now let us go a step further to show notifications when the log is an error log.
Implementing realtime notifications
First, we will create an Android service to listen if we receive any notification and display it accordingly.
Create a new file named NotificationsMessagingService
and paste this:
// File: ./app/src/main/java/com/example/androidloggerclient/NotificationsMessagingService.kt
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationManagerCompat
import com.google.firebase.messaging.RemoteMessage
import com.pusher.pushnotifications.fcm.MessagingService
class NotificationsMessagingService : MessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
val notificationId = 10
val channelId = "logs"
lateinit var channel:NotificationChannel
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val mBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(remoteMessage.notification!!.title!!)
.setContentText(remoteMessage.notification!!.body!!)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = applicationContext.getSystemService(NotificationManager::class.java)
val name = getString(R.string.channel_name)
val description = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
channel = NotificationChannel("log-channel", name, importance)
channel.description = description
notificationManager!!.createNotificationChannel(channel)
notificationManager.notify(notificationId, mBuilder.build())
} else {
val notificationManager = NotificationManagerCompat.from(this)
notificationManager.notify(notificationId, mBuilder.build())
}
}
}
The onMessageReceived
method in this service is alerted when a notification comes in. When the notification comes in, we display it to the user. Next, we need to register the notification service in the AndroidManifest.xml
file. You can do it by adding this to your file:
<!-- File: ./app/src/main/AndroidManifest.xml -->
<application
>
<!-- [...] -->
<service android:name=".NotificationsMessagingService">
<intent-filter android:priority="1">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
Next, let us setup Pusher beams in the MainActivity
file. Create a method like so:
private fun setupPusherBeams(){
PushNotifications.start(applicationContext, "PUSHER_BEAMS_INSTANCE_ID")
PushNotifications.subscribe("log-intrest")
}
Replace the placeholder above with the actual credentials from your dashboard.
This initializes Pusher beams and subscribes to the error-logs
interest. Next, add the method call to your onCreate
method in the MainActivity
class:
override fun onCreate(savedInstanceState: Bundle?) {
// [...]
setupPusherBeams()
}
If you now run your app, you should have something like this:
Conclusion
In this part, we have created the Android client for our logging monitoring. In the app, we display all logs being sent through the channels and the error logs are also sent as push notifications. In the next part of the series, we will create the iOS application for the log monitor.
The source code is available on GitHub.
27 March 2019
by Neo Ighodaro