Skip to content

WA4. Android Development in Kotlin – Some Advance Concepts

Part 1: Post Covid Attendance App

Statement 1

Scenario: Post-COVID, your organization has resumed office. Now, there is concern from the admin on how will the employees mark their attendance without touching the biometric (you are free to choose any kind viz. fingerprint, retina, face, and so on) machine. You are being asked to develop a biometric attendance application in Kotlin, using which an employee can mark his/her attendance without touching the biometric machine.

Assumption: All employees possess a smartphone that has biometric recognition sensors/components.

Tasks 1

  • Create a signup screen, where you will have some text fields for getting data from the user like name and email.
  • Put the validations on email, user should be able to proceed further only if entered email id is valid, else you should give an appropriate error message on the screen.
  • Create the next screen such that the user can set up his/her password.
  • Store data in the database after successful signup.
  • Show the sign-in screen to the user with two text fields to get the mail id and password.
    • If the credentials entered by the user match the ones he had given at signup, then they should be able to log in.
    • Otherwise, show an error message on the screen.
  • After successful sign-in, the user should land on the home screen of the app.
  • On the home screen, you have to put three buttons:
    1. To check-in
    2. To check-out
    3. To show the attendance in a list that the user marked previously.

Check In and Check Out Scenarios

  • Case 1: If the user has not registered his/her biometric, you need to show an alert asking the user to register their biometric with the app. When the biometric is registered successfully show a message that the biometric is registered successfully and save the biometric in the database.
  • Case 2: If the biometric is already registered, you have to ask for the biometric and when the user gives his biometric, you need to match those with the ones saved in the database.
    • If the biometric entered matches with the ones saved in the database, then check if the user is on office premises by matching the GPS location of his mobile phone with the office location. If the location matches, then you can let them check-in/out, otherwise, shows an appropriate error message. You also need to save the date and time for every successful check-in/out.
    • If biometrics do not match then show an appropriate message to the user.
  • Case 3: If the user already checked in/out for the day, they should not be able to check-in/out again and you need to give a proper message to the user in this case.
  • The third button which may be titled β€˜view attendance’, should let the user view his/her attendance on the screen that they marked previously.

Solution

Sig up Screen

  • Create a new project in Android Studio called AttendanceApp.
  • Below are the screenshots of the app.
  • Sing Up Screen Sing Up Screen

  • Sign Up with invalid email Sign Up with invalid email

  • Sign Up with existing email Sign Up with existing email

  • Correctly Sign Up Correctly Sign Up

Set Password Screen

  • Set up Password Screen Set up Password Screen

  • Successfully Sign In Successfully Sign In

Sign In Screen

  • Sign In Screen Sign In Screen

Home Screen

  • Home Screen Home Screen

Code

I will only include the code for the DbHelper class, other classes are sent with the zip folder

package com.example.attenedanceapp.data.models

data class User(
    val id: Int,
    val name: String,
    val email: String,
    val createdAt: String,
    val password: String?,
    val status: UserStatus
) {
    override fun toString(): String {
        return "User: id: $id, email: $email, name: $name, status: $status"
    }
}

enum class UserStatus {
    LOGGED_IN,
    CHECKED_IN,
    CHECKED_OUT,
    ADD_PASSWORD
}




data class Session (
    val id: Int,
    val userId: Int,
    val biometric: String,
    val createdAt: String,
    val updatedAt: String,
    val status: SessionStatus
    )


    enum class SessionStatus {
        CHECKED_IN,
        CHECKED_OUT,
    }
package com.example.attenedanceapp.data.db

import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.icu.text.DateFormat
import com.example.attenedanceapp.data.models.User
import com.example.attenedanceapp.data.models.UserStatus
import com.example.attenedanceapp.data.models.Session
import com.example.attenedanceapp.data.models.SessionStatus
import java.lang.Exception
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class DBHandler(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    companion object {
        private const val DB_NAME = "Attendance"
        private const val DB_VERSION = 1
        private const val USERS_TABLE_NAME = "users"
        private const val SESSIONS_TABLE_NAME = "sessions"
    }

    init {
       val db = writableDatabase
        println("πŸ‘‹ \uD83D\uDC4B \uD83D\uDC4B \uD83D\uDC4B \uD83D\uDC4B =============================")
       db.execSQL("DROP TABLE IF EXISTS $USERS_TABLE_NAME")
       db.execSQL("DROP TABLE IF EXISTS $SESSIONS_TABLE_NAME")
       this.onCreate(db)
    }

    override fun onCreate(db: SQLiteDatabase) {
        println("πŸ‘‰ Creating users table ....")
        val userTableDDL = """
            CREATE TABLE IF NOT EXISTS $USERS_TABLE_NAME (
                ID INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                email TEXT,
                createdAt TEXT,
                password TEXT,
                status TEXT
            )
        """
        db.execSQL(userTableDDL)

        val sessionsTableDDL = """
            CREATE TABLE IF NOT EXISTS $SESSIONS_TABLE_NAME (
                ID INTEGER PRIMARY KEY AUTOINCREMENT,
                day DATE,
                createdAt TEXT,
                biometric TEXT,
                status TEXT,
                userLd Long
            )
        """
        db.execSQL(sessionsTableDDL)
    }

    fun createUser(name: String, email: String): User? {
        val exist = this.getUserByEmail(email)

        if (exist != null) {
            println("user exist, returning:: ${exist}")
            return exist;
        }



        println("πŸ‘‰ creating user $email ....")

        val db = writableDatabase

        val values = ContentValues().apply {
            put("email", email)
            put("name", name)
            put("status", UserStatus.ADD_PASSWORD.toString())
            put("createdAt", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
        }

        val newId = db.insert(USERS_TABLE_NAME, null, values)
        println("new user id $newId")

        val u = this.getUserByEmail(email)
        db.close()
        return u
    }


    fun getUserByEmail(email: String): User? {
        val db = writableDatabase
        val cursor = db.rawQuery("SELECT * FROM $USERS_TABLE_NAME WHERE email = ?", arrayOf(email))
        println("πŸ‘‰ getting user $email ....")


        return if (cursor.moveToFirst()) {
            val idIndex = cursor.getColumnIndex("ID")
            val nameIndex = cursor.getColumnIndex("name")
            val createdAtIndex = cursor.getColumnIndex("createdAt")
            val passwordIndex = cursor.getColumnIndex("password")
            val statusIndex = cursor.getColumnIndex("status")

            if (idIndex == -1 && nameIndex == -1 && createdAtIndex == -1 && passwordIndex == -1 && statusIndex == -1) {
                throw Exception("User Not Found")
            }

            val id = cursor.getInt(idIndex)
            val name = cursor.getString(nameIndex)
            val createdAt = cursor.getString(createdAtIndex)
            val password = cursor.getString(passwordIndex) ?: ""
            val status =
                UserStatus.valueOf(cursor.getString(statusIndex)) ?: UserStatus.ADD_PASSWORD

            cursor.close()
            User(
                id, name, email, createdAt, password, status
            )
        } else {
            cursor.close()
            null
        }
    }

    fun setUserPassword(email: String, password: String): User? {
        val db = writableDatabase
        val values = ContentValues().apply {
            put("password", password)
            put("status", UserStatus.LOGGED_IN.toString())
        }

        val updated = db.update(USERS_TABLE_NAME, values, "email = ?", arrayOf(email))
        println("πŸ‘‰ setting user password $email ....")

        return if (updated > 0) {
            this.getUserByEmail(email)
        } else {
            null
        }
    }

    fun userLogin (email: String, password: String): User? {
        val db = writableDatabase
        val u = this.getUserByEmail(email)
        if(u == null) throw Exception("No user found")
        if(u.password != password) throw Exception("wrong password")
        val values = ContentValues().apply {
            put("status", UserStatus.LOGGED_IN.toString())
        }
         db.update(USERS_TABLE_NAME, values, "email = ?", arrayOf(email))
        return u;
    }


    fun userCheckIn (email: String): User? {
        val db = writableDatabase
        val u = this.getUserByEmail(email)
        if(u == null) throw Exception("No user found")
        val values = ContentValues().apply {
            put("status", UserStatus.CHECKED_IN.toString())
        }
        db.update(USERS_TABLE_NAME, values, "email = ?", arrayOf(email))
        return u;
    }

    fun createSession (biometric: String, user: User): Session? {
        val db = writableDatabase
        val values = ContentValues().apply {
            put("day", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE))
            put("createdAt", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
            put("biometric", biometric)
            put("status", SessionStatus.CHECKED_IN.toString())
            put("userLd", user.id)
        }

        val newId = db.insert(SESSIONS_TABLE_NAME, null, values)
        println("new session id $newId")
        return this.getTodaySession(user)

    }

    fun getTodaySession (user: User): Session? {
        val db = writableDatabase
        val cursor = db.rawQuery("SELECT * FROM $SESSIONS_TABLE_NAME WHERE userLd = ? AND day = ?", arrayOf(user.id.toString(), LocalDateTime.now().format(DateTimeFormatter.ISO_DATE)))
        println("πŸ‘‰ getting session for user ${user.email} ....")


        return if (cursor.moveToFirst()) {
            val idIndex = cursor.getColumnIndex("ID")
            val createdAtIndex = cursor.getColumnIndex("createdAt")
            val statusIndex = cursor.getColumnIndex("status")
            val userIdIndex = cursor.getColumnIndex("userLd")
            val updatedAtIndex = cursor.getColumnIndex("updatedAt")

            if (idIndex == -1 ) {
                throw Exception("Session Not Found")
            }

            val id = cursor.getInt(idIndex)
            val createdAt = cursor.getString(createdAtIndex)
            val updatedAt = cursor.getString(updatedAtIndex)
            val userId = cursor.getInt(userIdIndex)
            val status = SessionStatus.valueOf(cursor.getString(statusIndex)) ?: SessionStatus.CHECKED_IN

            cursor.close()
            Session(
                id,
                userId,
                biometric = "",
                createdAt,
                updatedAt,
                status
            )
        } else {
            cursor.close()
            null
        }

    }

    fun checkInSession (userId: String, biometric: String) {
        val db = writableDatabase
        val session = this.getTodaySession(User(userId.toInt(), "", "", "", "", UserStatus.LOGGED_IN))
        if(session == null) throw Exception("No session found")
        val values = ContentValues().apply {
            put("status", SessionStatus.CHECKED_IN.toString())
            put("biometric", biometric)
        }
        db.update(SESSIONS_TABLE_NAME, values, "ID = ?", arrayOf(session.id.toString()))
    }

    fun checkOutSession (userId: String, biometric: String) {
        val db = writableDatabase
        val session = this.getTodaySession(User(userId.toInt(), "", "", "", "", UserStatus.LOGGED_IN))
        if(session == null) throw Exception("No session found")
       if(session.status == SessionStatus.CHECKED_OUT) throw Exception("Session already checked out");
       if(session.biometric !== biometric) throw Exception("Different biometric was used to check in");

        val values = ContentValues().apply {
            put("status", SessionStatus.CHECKED_OUT.toString())
            put("biometric", biometric)
        }
        db.update(SESSIONS_TABLE_NAME, values, "ID = ?", arrayOf(session.id.toString()))
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("DROP TABLE IF EXISTS $USERS_TABLE_NAME")
        db.execSQL("DROP TABLE IF EXISTS $SESSIONS_TABLE_NAME")
        onCreate(db)
    }
}

Part 2: Network Connection App

Statement 2

  • Explain how we make an internet connection in an android application.
  • Demonstrate the same by developing a simple application that makes a successful network call and fetches some data from the internet.
  • Handle the following exceptions so that the app does not crash:
    • when there is no internet connection
    • when the network request is unable to fetch data

Tasks 2

  • Create a single-screen app, with a button on it that may be titled β€˜fetch data’.
  • With the tap of the button, you will be making the internet connection and will be calling the dummy API given in the question above.
  • Handle the following cases using the Kotlin exception handling concept:
    • when there will be no internet connection
    • when there will be no data fetched from the internet

Solution 2

  • The Android platform includes the HttpsURLConnection client, which supports TLS, streaming uploads and downloads, configurable timeouts, IPv6, and connection pooling.
  • Third party libraries: Retrofit (on top of OkHttp), and Ktor (kotlin native, uses coroutines, be JetBrains), all of these libraries wraps the HttpsURLConnection client and give us a more convenient API to work with.
  • The DnsResolver interface can be used to resolve host names to IP addresses, and vice versa.
  • Use the Repository pattern to abstract the data layer from the rest of the app, this is critical knowing how fast the Android ecosystem changes.
  • Network requests can NOT be done one the main thread, as a NetworkOnMainThreadException will be thrown.
  • Functions that perform network operations should be called from a coroutine or from a different thread (aka, should be suspend functions).
  • The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. That is, it holds component state between renderers (analogy to useState in React).
  • Best practices:

    • Before doing a request, check the network connection using ConnectivityManager or NetworkInfo.
    • Avoid large data transfers if device is not connected to a Wi-Fi network.
  • According to (Kamal, 2020) this is how you can check for network connectivity:

private fun isNetworkConnected(): Boolean {
  /*-1-*/ val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  /*-2-*/ val activeNetwork = connectivityManager.activeNetwork // enum indicating the type of network, null if no connection
  /*-3-*/ val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
  /*-4-*/ return networkCapabilities != null &&
        networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
  • The active network in the ConnectivityManager is an enum indicating the type of network, or null if no connection.
  • It can give you the connected network type: wifi, 4g, 3g, 5g, etc.
  • Avoid huge data transfers if device is not connected to a Wi-Fi network as this has a negative impact on the user experience.
  • You can utilize the client local database as a buffer during network outages or mobile data connections, and then once the user is connected to a wifi; the sync process can be triggered, where all local data is actually sent to the server.

References