Look Counter app graphics

Look Counter app is now in Kotlin and stores data in Room DB

In 2016 (which seems like a whole eternity from now :), I wrote an Android app called Look Counter for counting how many times you have switched the screen on, as well as unlocked it. Up until recently I haven’t updated it. The app used such libs as greenDAO , ButterKnife , GoogleAnalytics, and RecyclerView. After the update, only the last one remained 🙂

I’ve decided to completely re-write Look Counter, using the modern tools. The first step was to update all of the dependencies and set project’s target API to 27. It’s a good idea to set yours to 26 as a min, because of Google Play’s new requirements — “Google Play will require that new apps target at least Android 8.0 (API level 26) from August 1, 2018, and that app updates target Android 8.0 from November 1, 2018.”

As the second step I chose to refactor layouts and use ConstraintLayout in order to flatten the hierarchy. It allowed me to make both Calendar and About screens completely flat, which I think is awesome 🙂

Next I wanted to convert Java to Kotlin. I used Android Studio’s Convert Java file to Kotlin file option, and then cleaned up the code. I got rid of !! and used vals instead of vars whenever possible. Also, objects instead of classes, extension methods, etc. In general, simply converting Java to Kotlin is easy, but to make Kotlin code look good and not like some Java-adaption, requires an extra effort.

I also wanted to use some kind of a architecture, to organize logic and views separately. I just have three fragments — Main, Calendar and About in the app, but they had too much unorganized code inside of them. I’ve decided to go with an MVP — simple yet neat. Below is the example of Calendar contract class. You can immediately tell what to expect from the screen, just by looking at the list of methods. All the logic goes to the presenter, and UI rendering goes to the view.

interface CalendarContract {
    interface Presenter : BaseMvpPresenter<CalendarContract.View> {
        fun populateList(context: Context?, date: Calendar, screenLookCategory: ScreenLookCategory)
    }

    interface View : BaseView {
        fun setViewTitle(titleMonth: Calendar)

        fun setWeekLabels(weekLabels: List<String>)

        fun setDatesToCalendar(dates: List<Pair<Calendar, Int?>>)
    }
I wanted to move setContentView() from Activity or Fragment’s  onCreate(), so added it to the BaseView, like this:
interface BaseView {
    @LayoutRes
    fun getContentResource(): Int

    fun init(view: View, @Nullable state: Bundle?)
}
And in the fragment it looks like this:
class FragmentAbout : BaseFragment(), AboutContract.View {

    private lateinit var presenter: AboutPresenter

    override fun getContentResource() = R.layout.fragment_about

    override fun init(view: View, state: Bundle?) {
        presenter = AboutPresenter()
        presenter.attach(this)

        view.tv_about_text.text = (getString(R.string.about_text)).formatHtmlCompat()
        view.tv_about_looks.text = (getString(R.string.about_looks)).formatHtmlCompat()
        view.tv_about_unlocks.text = (getString(R.string.about_unlocks)).formatHtmlCompat()
        view.tv_about_me.text = (getString(R.string.about_me)).formatHtmlCompat()
        view.tv_about_source_code.movementMethod = LinkMovementMethod.getInstance()
        view.tv_about_source_code.text = (getString(R.string.about_source_code)).formatHtmlCompat()
        view.b_contact_me.setOnClickListener {
            context?.let {
                if (presenter.isAttached()) {
                    presenter.contactMe(it)
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        presenter.detach()
    }
}
In case of a database, I decided to give Google’s Room persistence library a try. It’s new and it works well with both RxJava and LiveData, and I wanted to use one of those as well, so the choice was obvious to me. Also, I wanted a lightweight library because of the app’s small size (~ 1.5 MB for a release version). And automatic handling of SQLiteOpenHelper was a plus.

Comparing to greenDAO, the integration was fast and simple. The only thing I had problems with was a correct database migration, without losing all the data. Because I had to re-created the same SQL code with Kotlin’s data class and Room’s annotations, as it was with Java and greenDAO. Finally, it worked and the data is persisted between app updates. This is how the data class for DAY_LOOK table looks like:

@Entity(tableName = "DAY_LOOK")
data class DayLookEntity(
        @PrimaryKey(autoGenerate = true)
        @ColumnInfo(name = "_id")
        var id: Long? = null,

        @ColumnInfo(name = "DATE")
        var date: String,

        @ColumnInfo(name = "SCREENON")
        var screenon: Int? = null,

        @ColumnInfo(name = "SCREENOFF")
        var screenoff: Int? = null,

        @ColumnInfo(name = "SCREENUNLOCK")
        var screenunlock: Int? = null

) {
    constructor() : this(
            id = null,
            date = "",
            screenon = null,
            screenoff = null,
            screenunlock = null
    )
}
Only date field is non-nullable and ids are generated automatically.

Below is the DAO interface with the list of queries that I use. All of them operate on DAY_LOOK table. I wanted inserting to work as a replace/update when the day look entry already exists, hence OnConflictStrategy.REPLACE.

getDayLookByDate() returns Maybe<DayLookEntity> because the entry may or may not exist, so an empty response is acceptable. getAllDayLooks() is the list of all entries in the table, so seemed logical to return a Flowable.

@Dao
interface DayLookDbDao {

    @Query("SELECT * FROM DAY_LOOK WHERE DATE = :dateToSearch LIMIT 1")
    fun getDayLookByDate(dateToSearch: String?): Maybe<DayLookEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insert(dayLook: DayLookEntity)

    @Query("SELECT * FROM DAY_LOOK")
    fun getAllDayLooks(): Flowable<List<DayLookEntity>>

    @Query("DELETE FROM DAY_LOOK")
    fun deleteAll()
}
Last, but not least, is the main DB class itself. I used a companion object in order to have a static equivalent of Java’s getInstance() method. ScreenCounterDb INSTANCE is a volatile one and is used within a double-locking mechanism (just 2 lines in Kotlin!). The migration method, though necessary, is empty, because I have’t changed the data’s integrity, only want to move it to version 2 of my DB (which happens to be a different library altogether 🙂 Without it, the whole data would be wiped out.
@Database(entities = [DayLookEntity::class], version = 2)
abstract class ScreenCounterDb : RoomDatabase() {

    private var dbClearFlag = false

    abstract fun dayLookDao(): DayLookDbDao

    companion object {

        @Volatile
        private var INSTANCE: ScreenCounterDb? = null
        private const val DB_NAME = "lookcount-db"

        private val FROM_1_TO_2 = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                // tables haven't changed, we're just moving from
                // greenDAO impl to Room
            }
        }

        fun getDatabase(context: Context): ScreenCounterDb =
                INSTANCE ?: synchronized(this) {
                    INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
                }

        private fun buildDatabase(context: Context) =
                Room.databaseBuilder(context.applicationContext,
                        ScreenCounterDb::class.java, DB_NAME)
                        .addMigrations(FROM_1_TO_2)
                        .build()
    }

    @Synchronized
    fun getDayLook(date: String): Maybe<DayLookEntity> {
        return if (dbClearFlag) {
            Maybe.just(DayLookEntity(null, date, null, null, null))
        } else {
            dayLookDao().getDayLookByDate(date)
        }
    }

    @Synchronized
    fun addDayLook(dayLook: DayLookEntity): Disposable {
        Log.d(C.TAG, "Insert/update daylook into the database.")

        return Completable.fromAction {
            dayLookDao().insert(dayLook)
        }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onComplete = {
                            Log.d(C.TAG, "addDayLook() insert() completed.")
                        }, onError = {
                            Log.e(C.TAG, "addDayLook() error: ${it.printStackTrace()}")
                })
    }

    @Synchronized
    fun dropDb() {
        this.dbClearFlag = false

        Completable.fromAction {
            dayLookDao().deleteAll()
            }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onError = {
                            Log.e(C.TAG, "dropDb() error: ${it.printStackTrace()}")
                        }
                )
    }

    @Synchronized
    fun setDbClearFlag(dbClearFlag: Boolean) {
        this.dbClearFlag = dbClearFlag
    }
}
By default Room’s queries are executed on the main thread, so you have to either add allowMainThreadQueries() to the database builder, use LiveData or RxJava/RxKotlin, otherwise an exception will be thrown. I chose the last option just for the purpose of learning RxKotlin 🙂 And it took me a while to make the app work as intended.

LookCounter screenshots

After all the above changes, what’s left was the counting service. I had to re-write it to become a foreground one, because really it’s the only way to make it work at all times and not get killed, when the resources are low, like a regular one is. I also had to add a few if’s, because of API differences (remember, there are now notification channels in Oreo? :). Other than that, the service just starts and stops the broadcast receiver which collects screen on/off/user present events and displays a notification in the system bar. Pressing the notification opens up the app and enables the user to check counter values, as well as stop the service manually.

/**
 * Service which starts a receiver for catching SCREEN_ON, SCREEN_OFF, and USER_PRESENT events.
 *
 * @author Antonina
 */
class LookCounterService : Service() {

    private val SERVICE_ID = 1
    private lateinit var screenLookReceiver: ScreenLookReceiver

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onCreate() {
        super.onCreate()
        registerScreenLookReceiver()
    }

    override fun onDestroy() {
        unregisterReceiver(screenLookReceiver)
        super.onDestroy()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForegroundServiceWithNotification()

        return super.onStartCommand(intent, flags, startId)
    }

    private fun registerScreenLookReceiver() {
        val filter = IntentFilter(Intent.ACTION_SCREEN_ON)
        with(filter) {
            addAction(Intent.ACTION_SCREEN_OFF)
            addAction(Intent.ACTION_USER_PRESENT)
        }

        screenLookReceiver = ScreenLookReceiver()
        registerReceiver(screenLookReceiver, filter)
    }

    private fun startForegroundServiceWithNotification() {
        val intent = Intent(this, ActivityMain::class.java)
        val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

        val builder: NotificationCompat.Builder
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channelName = getString(R.string.notification_channel_screen_looks)
            val importance = NotificationManager.IMPORTANCE_LOW
            val channelId = "LOOK_COUNTS_CHANNEL_ID"
            val channel = NotificationChannel(channelId, channelName, importance)
            channel.setShowBadge(false)

            builder = NotificationCompat.Builder(this, channelId)

            val notificatioManager = getSystemService(NotificationManager::class.java)
            notificatioManager.createNotificationChannel(channel)
        } else {
            builder = NotificationCompat.Builder(this)
        }

        val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) R.mipmap.ic_notification else R.mipmap.ic_launcher

        with(builder) {
            setContentTitle(getString(R.string.app_name))
            setContentText(getString(R.string.notification_service_description))
            setWhen(System.currentTimeMillis())
            setSmallIcon(icon)
            setContentIntent(pendingIntent)
            priority = NotificationCompat.PRIORITY_DEFAULT
            setVisibility(NotificationCompat.VISIBILITY_SECRET)
        }

        val notification = builder.build()

        startForeground(SERVICE_ID, notification)
    }
}
Just to sum up my 2 year old Android app re-writing experience, this is what I learned:

Conclusions on re-writing the app

  • Using a ConstraintLayout auto-convert option on a complex layout usually does’t work as intended. You often have to fix things manually and set constraints by yourself, as they’re all messed up.
  • Applying MVP architecture made the code easier to understand and navigate. It decoupled logic from UI and made things more clear in general.
  • Converting Java project to Kotlin is time consuming if you want to do it properly (without !! and by using objects, lateinits, extension functions 🙂 It is very pleasant though — to see how beautiful (and shorter) your code became!
  • greenDAO to Room migration went smoothly, but required writing all DB’s code from the scratch. Although classes look differently, underneath is the same SQL as before. Worth mentioning is that all queries are executed on the main thread by default, so I wrapped them into RxKotlin types.
  • Changing synchronous calls to asynchronous and using a push data approach instead of a pull one was really hard for me. I knew RxKotlin on a basic level and wasn’t sure what types to use and how to achieve what I wanted. Also, how to wrap DB calls and how to chain a few calls together? Unfortunately, there aren’t many tutorials on RxKotlin yet and those with RxJava demonstrate the usage of the older lib versions. Fortunately, after lots of attempts, I have worked my way through 🙂 I can recommend this project with a good set Rx exercises.

Source code:

Like and share:

Published by

Tonia Tkachuk

I'm an Android Developer, writing code for living and for fun. Love beautiful apps with a clean code inside. Enjoy travelling and reading.