Read & Write Call Logs in Android using Jetpack compose | Content Providers | Android Storage

Welcome back to all who have read my previous article on – How to read & write contacts in Android using Jetpack Compose. In this article we will be learning how to read and write Call Logs in device using your own app. Generally we don’t need to read call logs of device in our app but if you are planning to make apps like Backup & Restore of Call Logs or Calling apps like True caller then you might need such a feature in your app. So today we will learn from scratch to make a read and write call logs app.

To get started with this, you need a prior knowledge of Content Providers. To know more about Content providers you can read my article where i exlplained the every little details about content provider in android and build a demo app to create our custom content provider for app. You can read that here.

Content Providers

Content Providers are common storage which can be used by any when they want to make their data accessible by other apps. On contrary to other storage types like Internal Storage or Room Database or Shared Preferences, data stored in content providers can be accessed as well as modify data by any app with proper permission which is set by Content Provider app. All your system defaults apps like Contacts app, Call logs app, SMS app, Gallery app, File Manager app, Music app uses content provider for their data storage.

Before starting to make our contacts app you must know these things in brief about Content Provider or you can read full detailed article solely on Content provider here.

  1. Type of Data Stored- In CP you can store any of type of data whether its you apps raw data like Strings, Integers, Lists etc. as well as structured data like Images, Videos and Music files.
  2. Content URI- There are multiple Content Providers available on android device for each individual app. So, each content provider has its own address or say location which can be used by other apps to access the provider. In android we call this address as Content URI means its a Resource identification of a Content.
  3. Permissions- When an apps create its own content provider to stores his data for its own use then it must implement some permissions so that all apps don’t misuses the resources of this app.
  4. SQLite Database- It is lite version of SQL database which is used by android for offline or local storage for easy and fast data manipulations. Our content provider uses this same database for storing apps data. SQL Database uses SQL which is Structured Query Language used to do CRUD operations on database, which means content providers also uses same functions for its operations.
  5. Content Resolver- It is main link b/w a data seeker app and a content provider app. Android has this built in feature for security reasons that any app can’t directly use the content provider of other apps. Other apps need to interact with content resolver only and he will pass your query to content provider. Its a middle man for data exchange b/w apps.

First thing first, we will need a data class to store information about a particular call logs where we will store name of user, his mobile number, type of call (Incoming, Outgoing & Missed), duration of call and last the time & date of call.

data class CallLogItem(
    val name: String,
    val number: String,
    val type: Int,
    val date: Date,
    val duration: String
)

After this we have made a Call Logs helper class where we implemented two functions, one for reading call logs and other for writing call logs. In this class you need to pass context when you initialize it. Lets talk first talk about our ReadCallLogs function. This function will return us the List of our CallLogitem data class.

As you about content provider that you can’t directly access any content provider, for that you need Content Resolver, which is a middle man for your app and the provider app. So we need get instance of content resolver first. Also we have created a list variable where we will store our all contact info and finally we will return this list.

We will call the query function of content resolver which will take following data as inputs:

  1. Uri- we need to pass the Content URI means the address of our provider.
  2. projection : Array<String> – here you can pass the name of columns whose data you want to access or pass null to all all data from the table
  3. selection : String – it is like the where clause of our SQL query in which you can set a condition on any column like you want to fetch all contacts where email is ? (selection Args- our next parameter of query function)
  4. selectionArgs : Array<String> – here you can pass the arguments for the selection
  5. sortOrder : when you want to fetch you data in sorting order like ASC or DES based on any column then you pass that info here

For the projection means the column of our calls table we need only 5 which are name, number, type, duration and time only. In the selection criteria we want that only fetch call logs which don’t have a user name mean fetch all calls of unknown numbers. Finally for sorting order we will pass the Date in descending order means newer call on the top.

Our query will return us the Cursor which contains all our data in key values pair mean in list of maps. So need the column index for getting data for each row. In our cursor will run a while loop which will run until we have data in cursor and with each iteration we make a call logs item and add it into our list.

fun getCallLogs(): List<CallLogItem> {
    val callLogs = mutableListOf<CallLogItem>()

    val contentResolver = context.contentResolver

    val uri = CallLog.Calls.CONTENT_URI

    val projection = arrayOf(
        CallLog.Calls.CACHED_NAME,
        CallLog.Calls.NUMBER,
        CallLog.Calls.TYPE,
        CallLog.Calls.DATE,
        CallLog.Calls.DURATION
    )

    // Filter to fetch only unknown calls
    val selection = "${CallLog.Calls.TYPE} = ?"
    val selectionArgs = arrayOf(CallLog.Calls.INCOMING_TYPE.toString())

    val cursor: Cursor? = contentResolver.query(
        uri,
        projection, selection, selectionArgs,
        CallLog.Calls.DATE + " DESC"
    )

    cursor?.use {
        val nameIndex = it.getColumnIndex(CallLog.Calls.CACHED_NAME)
        val numberIndex = it.getColumnIndex(CallLog.Calls.NUMBER)
        val typeIndex = it.getColumnIndex(CallLog.Calls.TYPE)
        val dateIndex = it.getColumnIndex(CallLog.Calls.DATE)
        val durationIndex = it.getColumnIndex(CallLog.Calls.DURATION)

        while (it.moveToNext()) {
            val nameRow = it.getString(nameIndex)
            val name = if (nameRow == null || nameRow == "") "Unknown" else nameRow
            val number = it.getString(numberIndex)
            val type = it.getInt(typeIndex)
            val date = Date(it.getLong(dateIndex))
            val duration = it.getString(durationIndex) ?: "0"

            callLogs.add(CallLogItem(name, number, type, date, duration))
        }
    }
    return callLogs
}

Now come to the next function in our helper call which is insertCallLogs. In this function we need a CallLogItem as input parameter which contains the details for a call log. As you know that to insert data in content provider we need to make content Values for our data. After this simply call the insert function of content resolver and pass the URI or address of our call table and pass the content values.

@SuppressLint("MissingPermission")
fun insertCallLog(callLogItem: CallLogItem) {
    val values = ContentValues().apply {
        put(CallLog.Calls.CACHED_NAME, callLogItem.name)
        put(CallLog.Calls.NUMBER, callLogItem.number)
        put(CallLog.Calls.TYPE, callLogItem.type)
        put(CallLog.Calls.DATE, System.currentTimeMillis())
        put(CallLog.Calls.DURATION, callLogItem.duration)
    }
    context.contentResolver.insert(CallLog.Calls.CONTENT_URI, values)
}

Now our logic part is done, so come to the UI part for our app. For UI we need two composable functions for inserting and fetching the call logs. Lets first talk about insert composable which will take a lamda function as an argument and return a CallLogItem. This function will show 2 basic text field for get user name, number and call duration in seconds and then a drop down menu where you can select the type of call and finally a button which will make a CallLogItem from the user inputs and pass it in lamda function.

In order to show the Calls types in the dropdown menu we need two function where first function will convert Index into Call type and other will convert Call type into Index.

fun getIndexFromType(type: Int): Int {
    return when (type) {
        CallLog.Calls.INCOMING_TYPE -> 0
        CallLog.Calls.OUTGOING_TYPE -> 1
        CallLog.Calls.MISSED_TYPE -> 2
        else -> 0
    }
}

fun getCallTypeFromIndex(index: Int): Int {
    return when (index) {
        0 -> CallLog.Calls.INCOMING_TYPE
        1 -> CallLog.Calls.OUTGOING_TYPE
        2 -> CallLog.Calls.MISSED_TYPE
        else -> CallLog.Calls.INCOMING_TYPE
    }
}

This is our insert composable.

@Composable
fun InsertCallLogScreen(onSave: (CallLogItem) -> Unit) {
    var name by remember { mutableStateOf("") }
    var number by remember { mutableStateOf("") }
    var duration by remember { mutableStateOf("") }
    var selectedType by remember { mutableIntStateOf(CallLog.Calls.INCOMING_TYPE) }
    val callTypes = listOf("Incoming", "Outgoing", "Missed")

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(7.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Insert Call Log",
            style = MaterialTheme.typography.headlineSmall,
            fontWeight = FontWeight.Bold
        )

        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )

        OutlinedTextField(
            value = number,
            onValueChange = { number = it },
            label = { Text("Phone Number") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth(),
            keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone)
        )

        OutlinedTextField(
            value = duration,
            onValueChange = { duration = it },
            label = { Text("Duration (in sec)") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth(),
            keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number)
        )

        // Call Type Dropdown
        var expanded by remember { mutableStateOf(false) }
        Box {
            Text(
                text = callTypes[getIndexFromType(selectedType)], modifier = Modifier
                    .fillMaxWidth()
                    .clickable { expanded = true }
                    .border(1.dp, Color.Gray, RoundedCornerShape(4.dp))
                    .padding(16.dp)
            )

            DropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                callTypes.forEachIndexed { index, type ->
                    DropdownMenuItem(
                        onClick = {
                            selectedType = getCallTypeFromIndex(index)
                            expanded = false
                        }, text = {
                            Text(type)
                        }
                    )
                }
            }
        }

        // Save Button
        OutlinedButton(
            onClick = {
                val callLog = CallLogItem(
                    name = name,
                    number = number,
                    type = selectedType,
                    date = Date(),
                    duration = duration
                )
                onSave(callLog)
            },
            modifier = Modifier.wrapContentWidth()
        ) {
            Text("Save Call Log")
        }
    }
}

For show the UI of a call log item we have created a composable in which we have shown the first letter of name, then shows name, number, time and duration in a column composable and finally shown a icon for the type of call.

@Composable
fun CallLogItemRow(log: CallLogItem) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(6.dp)
            .background(Color.White, shape = RoundedCornerShape(8.dp))
            .border(1.dp, Color.LightGray, RoundedCornerShape(8.dp))
            .padding(4.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        // Circle with Initials
        Box(
            modifier = Modifier
                .size(50.dp)
                .background(Color.LightGray.copy(0.4f), CircleShape),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = log.name.firstOrNull()?.toString() ?: "U",
                style = MaterialTheme.typography.titleLarge,
                color = Color.Black
            )
        }

        Spacer(modifier = Modifier.width(12.dp))

        // Call Details Column
        Column(modifier = Modifier.weight(1f)) {
            Text(
                text = log.name,
                style = MaterialTheme.typography.bodyLarge,
                fontWeight = FontWeight.Bold
            )
            Text(
                text = log.number,
                style = MaterialTheme.typography.bodyMedium,
                color = Color.Gray
            )
            Text(
                text = SimpleDateFormat(
                    "dd MMM yyyy, hh:mm:ss a",
                    Locale.getDefault()
                ).format(log.date),
                style = MaterialTheme.typography.bodySmall,
                color = Color.Gray
            )
            Text(
                text = "${log.duration} sec",
                style = MaterialTheme.typography.bodySmall,
                color = Color.DarkGray
            )
        }

        // Call Type Icon
        Icon(
            painter = when (log.type) {
                CallLog.Calls.INCOMING_TYPE -> painterResource(id = R.drawable.round_call_received_24)
                CallLog.Calls.OUTGOING_TYPE -> painterResource(id = R.drawable.baseline_call_made_24)
                CallLog.Calls.MISSED_TYPE -> painterResource(id = R.drawable.round_call_missed_24)
                else -> painterResource(id = R.drawable.round_call_received_24)
            },
            contentDescription = "Call Type",
            tint = when (log.type) {
                CallLog.Calls.MISSED_TYPE -> Color.Red
                else -> Color(0xFF4CAF50)  // Green for Incoming/Outgoing
            },
            modifier = Modifier
                .align(Alignment.CenterVertically)
                .size(24.dp)
        )
    }
}

After this we also need a function to ask the Read and Write permission of call logs. This function will take a lamda as input, if all the premissions granted then we will call this lamda.

@Composable
fun RequestPermissions(
    onGranted: () -> Unit
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val allGranted = permissions.values.all { it }
        if (allGranted) {
            onGranted()
        }
    }

    LaunchedEffect(Unit) {
        launcher.launch(
            arrayOf(
                Manifest.permission.READ_CALL_LOG,
                Manifest.permission.WRITE_CALL_LOG
            )
        )
    }
}

Now come to our final code in MainActivity where we need to wrap up all the code and do final implementation of the code. First initialize the Helper class in remember block with context. Then create a variable which maintains the status of permission, if permission is granted then we will show the UI otherwise show a text that all permission are required. Call this code in the setContent block of main activity.

val callLogHelper = remember { CallLogHelper(this) }
var permissionGranted by remember { mutableStateOf(false) }

val callLogs = remember { mutableStateListOf<CallLogItem>() }

RequestPermissions {
    permissionGranted = true
}

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {


    if (permissionGranted) {
        LaunchedEffect(Unit) {
            callLogs.addAll(callLogHelper.getCallLogs())
        }

        InsertCallLogScreen { callLogItem ->
            callLogHelper.insertCallLog(callLogItem)
            callLogs.clear()
            callLogs.addAll(callLogHelper.getCallLogs())
        }

        LazyColumn {
            items(callLogs) { log ->
                CallLogItemRow(log)
            }
        }
    } else {
        Text("Permission Required to Access Call Logs")
    }
}

Finally don’t forget to add these 2 permissions in app manifest file.

<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />

For full code see this GitHub Gist here

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top