Free PacktPub Ebook Notifier App In Android (Kotlin)

Android App to Notify Free Ebook from PacktPub Every day using Kotlin.

Introduction

PacktPub is one of the greatest ebook and videos sites for tech users and they offer one free ebook every day. Most of the time, I forget to visit the site so I miss a lot of free good ebooks and regret it later. So, I decided to create a weekend side project to explore the Kotlin language and also to refresh my Android skills by understanding the latest Andriod O background processing limitations.

It took some time to understand the basic Kotlin language concepts (like companion objects, null safety) but its a good opportunity for .Net developers to explore this language rather than coding in Java for Android apps. Kotlin also provides some real good extensions plugins to eliminate lots of boilerplate Android code like findviewbyid.

Application Overview

 

This app launches the main activity with asynctask in background to parse the content to get the free ebook title, image url and render it on the main activity. It also checks the site periodically (based on settings) and notifies the user.

  1. class MainActivity : AppCompatActivity(),ToolbarManager {  
  2.   
  3.     private val Tag: String = "MainActivity"  
  4.     private lateinit var parserHelper: ParserHelper  
  5.     private lateinit var alarmManagerHelper: AlarmManagerHelper  
  6.     var mImageURL: String? = null  
  7.     var mTitle: String? = null  
  8.     override val toolbar by lazy { find(R.id.toolbar) }  
  9.   
  10.     override fun onCreate(savedInstanceState: Bundle?) {  
  11.         super.onCreate(savedInstanceState)  
  12.         setContentView(R.layout.activity_main)  
  13.         initToolbar()  
  14.         toolbarTitle = getString(app_name)  
  15.   
  16.         claimThisBook.setOnClickListener {  
  17.             val intent = Intent(Intent.ACTION_VIEW)  
  18.                     .setData(Uri.parse(getString(R.string.FREE_BOOK_URL)))  
  19.             startActivity(intent)  
  20.         }  
  21.   
  22.         //Call the Async Task  
  23.         LoadContentTask().execute()  
  24.   
  25.         //Setting the Broadcast Alert  
  26.         alarmManagerHelper = AlarmManagerHelper(this)  
  27.         alarmManagerHelper.setBroadCastAlert(false)  
  28.   
  29.         //Enable the BootReceiver  
  30.         enableBootReceiver()  
  31.     }  
  32.   
  33.     private fun enableBootReceiver() {  
  34.         val receiver = ComponentName(this, BootReceiver::class.java)  
  35.         val pm = this.packageManager  
  36.         pm.setComponentEnabledSetting(receiver,  
  37.                 PackageManager.COMPONENT_ENABLED_STATE_ENABLED,  
  38.                 PackageManager.DONT_KILL_APP)  
  39.     }  
  40.   
  41.     inner class LoadContentTask : AsyncTask() {  
  42.         override fun doInBackground(vararg p0: Unit) {  
  43.             parserHelper = ParserHelper(this@MainActivity);  
  44.             val (title, imageURL) = parserHelper.parsePacktPubFreeBook()  
  45.             mTitle = title  
  46.             mImageURL = imageURL  
  47.         }  
  48.   
  49.         override fun onPostExecute(result: Unit) {  
  50.             super.onPostExecute(result)  
  51.             Glide.with(this@MainActivity).load(mImageURL).into(imageView)  
  52.             titleView.text = mTitle  
  53.         }  
  54.     }  
  55. }  

I have created AlarmManagerHelper class to set a repeating alarm based on the sync frequency settings. The Sync Frequency Settings are stored in SharedPreferences. 

  1. internal class AlarmManagerHelper(ctx: Context) : ContextWrapper(ctx) {  
  2.   
  3.     fun setBroadCastAlert(argRefreshAlarm: Boolean) {  
  4.   
  5.         var refreshAlarm = argRefreshAlarm  
  6.         val alarmManager = this.getSystemService(Activity.ALARM_SERVICE) as AlarmManager  
  7.   
  8.         val notificationIntent = Intent(Constants.ALARM_RECEIVER_INTENT_TRIGGER)  
  9.         notificationIntent.setClass(this, NotificationReceiver::class.java)  
  10.   
  11.         var pendingIntent = PendingIntent.getBroadcast(this0, notificationIntent, PendingIntent.FLAG_NO_CREATE)  
  12.         if (pendingIntent == null) {  
  13.             pendingIntent = PendingIntent.getBroadcast(this0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)  
  14.             refreshAlarm = true  
  15.         }  
  16.         if (refreshAlarm) {  
  17.             val syncFrequency: String by DelegatesExt.preference(this, Constants.SYNC_FREQUENCY_NAME, Constants.SYNC_FREQUENCY_DEFAULT_VALUE)  
  18.             val syncFrequencyLong = syncFrequency.toLong()  
  19.             var finalCalInMillis: Long = System.currentTimeMillis() + (syncFrequencyLong * AlarmManager.INTERVAL_HOUR)  
  20.             alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, finalCalInMillis, (syncFrequencyLong * AlarmManager.INTERVAL_HOUR) , pendingIntent)  
  21.         }  
  22.     }  
  23. }  

From Android O, there are lots of limitations on background execution limits and you can read all those details in the Android Developer Guide. For this project, I need a background job that should run periodically based on frequency settings defined in the app and send the notification to user with the book title. I used JobIntentService to do the background work and send the notification to the user.

  1. class NotificationService : JobIntentService() {  
  2.   
  3.     private lateinit var notiHelper: NotificationHelper  
  4.     private lateinit var parserHelper: ParserHelper  
  5.   
  6.     companion object {  
  7.         private const val JOB_ID = 1000  
  8.         private const val NOTI_PRIMARY = 1100  
  9.   
  10.         fun enqueueWork(ctx: Context, intent: Intent) {  
  11.             enqueueWork(ctx, NotificationService::class.java, JOB_ID, intent)  
  12.         }  
  13.     }  
  14.   
  15.     override fun onHandleWork(intent: Intent) {  
  16.   
  17.         notiHelper = NotificationHelper(this)  
  18.         parserHelper = ParserHelper(this);  
  19.   
  20.         val(title) = parserHelper.parsePacktPubFreeBook()  
  21.   
  22.         val pendingIntent = PendingIntent.getActivity(this0, intent, 0)  
  23.         notiHelper.notify(NOTI_PRIMARY, notiHelper.getNotification(getString(R.string.noti_title), title,pendingIntent))  
  24.     }  
  25. }  

I also created BootReceiver with BOOT_COMPLETED intent so that repeating alarm will get set even in the event of a phone restart.

  1. class BootReceiver : BroadcastReceiver() {  
  2.   
  3.     private lateinit var alarmManagerHelper: AlarmManagerHelper  
  4.   
  5.     override fun onReceive(context: Context, intent: Intent) {  
  6.         if (intent.action == "android.intent.action.BOOT_COMPLETED") {  
  7.             alarmManagerHelper = AlarmManagerHelper(context)  
  8.             alarmManagerHelper.setBroadCastAlert(false)  
  9.         }  
  10.     }  
  11. }  

I used Jsoup Library to parse the HTML content. Jsoup is a Java library for working with real-world HTML. It provides a very convenient API for extracting and manipulating data, using the best of DOM, CSS, and jquery-like methods.

  1. internal class ParserHelper(ctx: Context) : ContextWrapper(ctx) {  
  2.   
  3.     private val tag: String = "ParserHelper"  
  4.   
  5.     data class ParserEntity(val title: String, val imageURL: String?)  
  6.   
  7.     fun parsePacktPubFreeBook(): ParserEntity {  
  8.         var title = "Internet access not available"  
  9.         var imageURL: String? = null  
  10.         if (isInternetConnected()) {  
  11.             try {  
  12.                 val htmlContent = Jsoup.connect(getString(R.string.FREE_BOOK_URL)).get()  
  13.                 title = htmlContent.select("div[class=dotd-title]").text()  
  14.                 imageURL = "http:" + htmlContent.select("img[class=bookimage imagecache imagecache-dotd_main_image]").attr("src")  
  15.             } catch (e: Exception) {  
  16.                 Log.e(tag, "Error in fetching the content " + e.printStackTrace())  
  17.             }  
  18.         }  
  19.   
  20.         return ParserEntity(title, imageURL)  
  21.     }  
  22.   
  23.     private fun isInternetConnected(): Boolean {  
  24.         val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
  25.         val activeNetwork = cm.activeNetworkInfo  
  26.         return activeNetwork != null && activeNetwork.isConnectedOrConnecting  
  27.     }  
  28. }  

So, overall this project will give you an idea of how to develop an Android app in Kotlin including running the background job and sending the notitfication to users. I have uploaded the entire source code in github.

You can also find my other articles in my blog.

Happy Coding!!