Read & Write SMS Using Content Provider with Jetpack Compose | Kotlin

Reading SMS from a device is not necessary for all apps but some apps like SMS apps or SMS Backup apps need this kind of functionality to read and write SMS from device. Today we will create a SMS app which will read all the SMS from the android device with proper user permission and show them in a list and you can insert new message into the device with little hardwork(Concept of Default Handlers).

We are going to use content providers to fetch and insert messages into the device. Before starting to make our SMS app you must know these things in brief about Content Provider or you can read full detailed article solely on Content provider 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.

  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 of all we need a data class which will hold the basic info about a SMS like

  1. address- Contact number with which conversation happens
  2. body – Content of message
  3. date – exact date and time of message received/send
data class SMSModel(
    val address: String,
    val body: String,
    val date: String
)

After this we need some variables which will hold the state of our SMS app like messages- a list of SMSModel and two string variable which hold the data for our two textfield namely Address and body. Then we have called a LaunchedEffect in which our getAllSMS function will be called and store all SMS in the messages variable.

var messages by remember { mutableStateOf<List<SMSModel>>(emptyList()) }
var phoneNumber by remember { mutableStateOf("") }
var messageBody by remember { mutableStateOf("") }
val context = LocalContext.current

LaunchedEffect(Unit) {
    messages = getAllSMS(context)
}

Then we have show our UI where there are two text fields and two buttons, one for inserting message and other one is to change default SMS handler. Lets talk about the default handlers in android.

Default Handlers

In Android, a default handler refers to the app designated by the user or system to handle a specific type of action or intent. These handlers ensure that certain types of user actions (e.g., making calls, sending SMS, browsing websites) are consistently managed by the same app.

Some examples of default handlers in Android include:

  1. Dialer App: Manages phone calls.
  2. SMS App: Sends and receives text messages.
  3. Browser App: Opens web links.
  4. Email App: Handles email messages.
  5. Home App (Launcher): Manages the home screen and app navigation.
  6. Assistant App: Responds to voice queries or commands.

Default SMS Handler

The Default SMS Handler is the app chosen by the user to send, receive, and manage SMS or MMS messages. Only the app set as the default SMS handler can:

  • Access the SMS database directly.
  • Delete or modify SMS/MMS messages.
  • Respond to SMS-related system events like SmsReceiver.

Key Features of a Default SMS Handler

  1. Direct Access to SMS/MMS Messages:
    • The default SMS app has exclusive read and write access to the system’s SMS/MMS database.
    • Non-default apps can only send SMS using SmsManager, but they can’t access the database.
  2. Send and Receive SMS:
    • The app can send messages using SmsManager.
    • It receives incoming SMS via a BroadcastReceiver for the SMS_RECEIVED event.
  3. Handle User Interactions:
    • As the default handler, it intercepts user actions like clicking on SMS notifications.
  4. Permissions:
    • Requires the SEND_SMS and RECEIVE_SMS permissions.
    • Can modify the database with WRITE_SMS (available only to the default handler).

We have created one functions to change default SMS handler. This function will first check the current android API level then change default SMS handler accordingly as before API level 29 we can automatically change handlers without user selection but after that android shows us a dialog with list of eligible app to become default handlers.

private fun requestDefaultSmsPackageChange(activity: Activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val roleManager = activity.getSystemService(ROLE_SERVICE) as RoleManager
        if (!roleManager.isRoleHeld(ROLE_SMS)) {
            val intent = roleManager.createRequestRoleIntent(ROLE_SMS)
            activity.startActivityForResult(intent, 1)
        }
    } else {
        val intent = Intent(ACTION_CHANGE_DEFAULT).putExtra(
            EXTRA_PACKAGE_NAME, activity.packageName
        )
        activity.startActivityForResult(intent, 1)
    }
}

The button to change default SMS handler do two actions based on the condition like if our app is default handler for SMS then it shows a dialog then if your app has completed its work as default SMS handler then intimate user that he can switch to his main SMS app. For example, user has installed you app to backup restore SMS and he has successfully restored all SMS then, the user must switch back to his previous SMS app because you app has not full features to handle all SMS related activity.

fun restoreDefaultSmsProvider(activity: Activity) {
    MaterialAlertDialogBuilder(activity)
        .setTitle("Change Default SMS Handler")
        .setMessage("If you have successfully restored your SMS then make previous SMS app as default SMS handler")
        .setCancelable(false)
        .setNegativeButton(
            "No"
        ) { dialog, which ->
            dialog.dismiss()
        }
        .setPositiveButton(
            "Yes"
        ) { dialog, id ->
            val intent = Intent(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_APP_MESSAGING)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            activity.startActivity(intent)
        }
        .show()

}

Now have a look at out getAllSms function which is using content provider and fetching all SMS and provide us a list of SMSModel.

fun getAllSMS(context: Context): List<SMSModel> {

    val smsList = mutableListOf<SMSModel>()

    val cursor = context.contentResolver.query(
        Telephony.Sms.CONTENT_URI,
        arrayOf(
            Telephony.Sms.ADDRESS,
            Telephony.Sms.BODY,
            Telephony.Sms.DATE
        ),
        null,
        null,
        Telephony.Sms.DEFAULT_SORT_ORDER
    )

    cursor?.use {
        val addressIndex = it.getColumnIndex(Telephony.Sms.ADDRESS)
        val bodyIndex = it.getColumnIndex(Telephony.Sms.BODY)
        val dateIndex = it.getColumnIndex(Telephony.Sms.DATE)

        while (it.moveToNext()) {
            val address = it.getString(addressIndex)
            val body = it.getString(bodyIndex)
            val date = java.util.Date(it.getLong(dateIndex)).toString()

            smsList.add(SMSModel(address, body, date))
        }
    }

    return smsList
}

then our insert function will take the context, SMS address and SMS body as inputs and insert the data into the SMS table by calling content resolver.

fun insertSMS(context: Context, address: String, body: String) {
    val values = ContentValues().apply {
        put(Telephony.Sms.ADDRESS, address)
        put(Telephony.Sms.BODY, body)
        put(Telephony.Sms.DATE, System.currentTimeMillis())
        put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_INBOX)
    }

    context.contentResolver.insert(Telephony.Sms.CONTENT_URI, values)
}

To show our SMSModel items in a list we have created a separate composable name MessageCard which will show our each message UI.

@Composable
fun MessageCard(message: SMSModel) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
    ) {
        Column(
            modifier = Modifier
                .padding(7.dp)
        ) {
            Text(
                text = "From: ${message.address}",
                fontSize = 15.sp,
                fontWeight = FontWeight.Medium
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(
                text = message.body,
                fontSize = 13.sp,
                fontStyle = FontStyle.Italic,
                fontWeight = FontWeight.Light
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(
                text = "Date: ${message.date}",
                fontSize = 14.sp,
                fontStyle = FontStyle.Normal,
                fontWeight = FontWeight.SemiBold
            )
        }
    }
}

This is our final compete UI for the app

Column(
   modifier = Modifier
              .fillMaxSize()
              .padding(4.dp)
) {
    // Input Section
    OutlinedTextField(
        value = phoneNumber,
        onValueChange = { phoneNumber = it },
        label = { Text("Phone Number") },
        modifier = Modifier.fillMaxWidth()
    )

    Spacer(modifier = Modifier.height(8.dp))

    OutlinedTextField(
        value = messageBody,
        onValueChange = { messageBody = it },
        label = { Text("Message") },
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    )

    Spacer(modifier = Modifier.height(16.dp))

    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Button(
            onClick = {
                insertSMS(context, phoneNumber, messageBody)
                phoneNumber = ""
                messageBody = ""
                messages = getAllSMS(context)
            }
        ) {
            Text("Save SMS")
        }

        Button(
            onClick = {
                if (Telephony.Sms.getDefaultSmsPackage(context) != null &&
                    Telephony.Sms.getDefaultSmsPackage(context) == context.packageName
                ) {
                    restoreDefaultSmsProvider(activity)
                } else {
                    MaterialAlertDialogBuilder(activity)
                        .setTitle("Alert!")
                        .setMessage("This app needs to be temporarily set as the default SMS app to restore SMS.\n * After restoring all sms backup please change your default SMS handler.")
                        .setCancelable(false)
                        .setNegativeButton(
                            "No"
                        ) { dialog, which ->
                            dialog.dismiss()
                        }
                        .setPositiveButton(
                            "Yes"
                        ) { dialog, id ->
                            requestDefaultSmsPackageChange(activity)
                        }
                        .show()
                }
            }
        ) {
            Text("Change Default SMS handler")
        }
    }

    Spacer(modifier = Modifier.height(16.dp))

    // Messages List
    LazyColumn {
        items(messages) { message ->
            MessageCard(message)
        }
    }
}

then use our permission handling code from the previous video. In this function we will pass the permission we need to access the SMS. If all the permission are granted by user then call a lamda which we use in our main activity

@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_SMS,
            )
        )
    }
}

Now come to our main activity and show the UI

setContent {
    var permissionGranted by remember { mutableStateOf(false) }


    RequestPermissions {
        permissionGranted = true
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (permissionGranted) {
            SMSApp(this@MainActivity)
        } else {
            Text("Permission Required to Access Call Logs")
        }
    }
}

Now our final task is to define permission in the manifest file. When we define any permission which also require use of android hardware then we need to define the uses-featues meta tag which will tell android system that your app will also use some kind of hardware source like when we ask READ_SMS permission then we use the telephony hardware.

<!--Read and write sms-->
<uses-permission android:name="android.permission.READ_SMS" />

<uses-feature
   android:name="android.hardware.telephony"
   android:required="false" />

After this, if you want that you app should be elligible to become default SMS handler then it should have some features which are required by a default SMS handler like listeners for incoming/outgoing SMS and MMS, a dedicated activity which handle all UI for sending, receiving SMS and a dedicated service which handle all kind of background task to manage messages. so lets create them one by one and define them in our manifest file so that android consider your app the next time you show dialog to choose a SMS handler.

public class HeadlessSmsSendService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

public class MmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

    }
}

public class SmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

    }
}

public class ComposeSmsActivity extends Activity {
}

lets define them in manifest file

    <!-- BroadcastReceiver that listens for incoming SMS messages -->
<receiver
android:name=".smsapp.SmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
    <action android:name="android.provider.Telephony.SMS_DELIVER" />
</intent-filter>
</receiver>

    <!-- BroadcastReceiver that listens for incoming MMS messages -->
<receiver
android:name=".smsapp.MmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_WAP_PUSH">
<intent-filter>
    <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
    <data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>

    <!-- Activity that allows the user to send new SMS/MMS messages -->
<activity
android:name=".smsapp.ComposeSmsActivity"
android:exported="true">
<intent-filter>
    <action android:name="android.intent.action.SEND" />
    <action android:name="android.intent.action.SENDTO" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="sms" />
    <data android:scheme="smsto" />
    <data android:scheme="mms" />
    <data android:scheme="mmsto" />
</intent-filter>
</activity>

    <!-- Service that delivers messages from the phone "quick response" -->
<service
android:name=".smsapp.HeadlessSmsSendService"
android:exported="true"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE">
<intent-filter>
    <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
    <category android:name="android.intent.category.DEFAULT" />

    <data android:scheme="sms" />
    <data android:scheme="smsto" />
    <data android:scheme="mms" />
    <data android:scheme="mmsto" />
</intent-filter>
</service>

This is all for this article but if you want complete code then it here

Leave a Comment

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

Scroll to Top