Its been a while google restricted the permissions of sms/mms reading and sending. But if your app’s core functionality depends on these permissions you might get an exception approval. So how do we make it? Here we will learn how to do the android programming and gradually we will make an awesome app in android. Lets get started.
Declare as default sms app:
From android kitkat(4.4) only one sms app can be the default app in os. Only one app will handle all the sms functionality. So users have to select or choose the built in app or any other third party app from the list. You app must have to participate in that list. In order to do so we have to use some permissions in maniefiest.xml
file and use some services and broadcast listeners. So go ahead, create your project.
We will create an activity and name it StarterActivity
. The functions of this activity is to find and check the current default sms app and depending on that takes the next steps. We will add a button and a textview in the layout. To check the current app. We will use the code below in onResume
block.
@Override protected void onResume() { super.onResume(); final String myPackageName = getPackageName(); if (Telephony.Sms.getDefaultSmsPackage(getApplicationContext()) == null){ startActivity(new Intent(this, HomeActivity.class)); finish(); }else { if (!Telephony.Sms.getDefaultSmsPackage(getApplicationContext()).equals(myPackageName)) { // App is not default. // Show the "not currently set as the default SMS app" interface // Set up a button that allows the user to change the default SMS app Button button = findViewById(R.id.bt_set_default); button.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, myPackageName); startActivity(intent); } }); } else { // App is the default. // Hide the "not currently set as the default SMS app" interface startActivity(new Intent(this, HomeActivity.class)); finish(); } } }
Now to show our app in the list we will add two broadcast listeners. Create a package name broadcast and in that package create two new broadcast listener classes. You can name it SMSReceiver
and another is MMSReceiver
. When the phone receives a sms or mms these classes will be invoked accordingly. Now again create a package and name it services. In this package we will add a service class. We can name it as QuickResponseService
. From the name we are able to guess the task of this class. This will be invoked when an user sends the quick sms from the incoming call.
These classes are not fully functional yet. Go to your AndroidManifest
file and change the recently added broadcast listener and service according to below code.
SMSReceive:
<!-- BroadcastReceiver that listens for incoming SMS messages --> <receiver android:name=".broadcast.SMSReceiver" android:enabled="true" android:exported="true" android:permission="android.permission.BROADCAST_SMS"> <intent-filter> <action android:name="android.provider.Telephony.SMS_DELIVER" /> </intent-filter> </receiver>
MMSReceiver:
<!-- BroadcastReceiver that listens for incoming SMS messages --> <receiver android:name=".broadcast.MmsReceiver" 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>
QuickResponseService:
<!-- Service that delivers messages from the phone "quick response" --> <service android:name=".services.QuickResponseService" 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>
Now run your app again and test it. Click on the button. In that default sms list you app should be there. You can select your app as default sms handler now.
Your app should look like this:
Handle incoming SMS
As you can see we are trying to build a default sms app that’s why we have so many responsibilities like receiving sms, check it if it’s from blocked number or not, write it to sms provider, show notification and many more. We will see it step by step.
So like I said, the SMSReceiver
broadcast class will be invoked when we have the an incoming sms.
Here is the code:
public class SmsReceiver extends BroadcastReceiver { private static final String CHANNEL_ID = "mysms"; @Override public void onReceive(Context context, Intent intent) { //Log.e("sms", "mms"); // Retrieves a map of extended data from the intent. final Bundle bundle = intent.getExtras(); try { if (bundle != null) { final Object[] pdusObj = (Object[]) bundle.get("pdus"); for (int i = 0; i < pdusObj.length; i++) { SmsMessage currentMessage = SmsMessage.createFromPdu((byte[]) pdusObj[i]); String senderNum = currentMessage.getDisplayOriginatingAddress(); String message = currentMessage.getDisplayMessageBody(); Spammer spammer = Constants.getDbHelper(context).getSpammerByNumber(senderNum); if(spammer == null){ saveSms(context, senderNum, message, 0, System.currentTimeMillis(), "inbox"); }else { SpamBox spamBox = new SpamBox(); spamBox.setBody(message); spamBox.setDate(System.currentTimeMillis()); spamBox.setCreated_at(System.currentTimeMillis()); spamBox.setUpdated_at(System.currentTimeMillis()); spamBox.setSpammer_id(spammer.getId()); Constants.getDbHelper(context).addToSmapBox(spamBox); showBlockedSmsNotification(context, message, senderNum); break; } } // end for loop } // bundle is null } catch (Exception e) { Log.e("SmsReceiver", "Exception smsReceiver" +e); } } public boolean saveSms(Context context, String phoneNumber, String message, int readState, long time, String folderName) { boolean ret = false; try { //Telephony.Sms.MESSAGE_TYPE_DRAFT ContentValues values = new ContentValues(); values.put(Telephony.Sms.ADDRESS, phoneNumber); values.put(Telephony.Sms.BODY, message); values.put(Telephony.Sms.READ, readState); //"0" for have not read sms and "1" for have read sms values.put(Telephony.Sms.DATE, time); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Uri uri = Telephony.Sms.Sent.CONTENT_URI; if(folderName.equals("inbox")){ uri = Telephony.Sms.Inbox.CONTENT_URI; } context.getContentResolver().insert(uri, values); } else { context.getContentResolver().insert(Uri.parse("content://sms/" + folderName), values); } ret = true; OnMessage om = new OnMessage(phoneNumber, message, readState, time); EventBus.getDefault().post(om); showNotification(context, message, phoneNumber); } catch (Exception ex) { ex.printStackTrace(); ret = false; } return ret; } public String getContactName(Context context, final String phoneNumber) { Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); String[] projection = new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME}; String contactName = phoneNumber; Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null); if (cursor != null) { if (cursor.moveToFirst()) { contactName = cursor.getString(0); } cursor.close(); } return contactName; } private void showNotification(Context context, String msg, String address) { Intent notificationIntent; String name = getContactName(context, address); notificationIntent = new Intent(context, MessageDetailsActivity.class); notificationIntent.putExtra("header", address); notificationIntent.putExtra("body", msg); notificationIntent.putExtra("reply", true); notificationIntent.putExtra("name", name); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent contentIntent = PendingIntent.getActivity(context, (int)System.currentTimeMillis() / 3600, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setContentTitle(name) .setContentText(msg) .setSmallIcon(R.drawable.ic_stat_name) .setAutoCancel(true) .setStyle(new NotificationCompat.BigTextStyle()) .setColor(ContextCompat.getColor(context, R.color.colorPrimaryDark)) .setChannelId(CHANNEL_ID) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setContentIntent(contentIntent) //.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE) .setPriority(NotificationManager.IMPORTANCE_HIGH); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ALARM) .build(); String Description = msg; int importance = NotificationManager.IMPORTANCE_HIGH; NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance); mChannel.setDescription(Description); mChannel.enableLights(true); mChannel.setLightColor(Color.RED); mChannel.enableVibration(true); mChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), audioAttributes); mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); mChannel.setShowBadge(false); notificationManager.createNotificationChannel(mChannel); } notificationManager.notify((int) System.currentTimeMillis() / 3600, builder.build()); } private void showBlockedSmsNotification(Context context, String msg, String address) { Intent notificationIntent; String name = getContactName(context, address); notificationIntent = new Intent(context, MessageDetailsActivity.class); notificationIntent.putExtra("header", address); notificationIntent.putExtra("body", msg); notificationIntent.putExtra("reply", true); notificationIntent.putExtra("name", name); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setContentTitle("SMS blocked: " + name) .setContentText(msg) .setSmallIcon(R.drawable.ic_stat_name) .setAutoCancel(true) .setStyle(new NotificationCompat.BigTextStyle()) .setColor(ContextCompat.getColor(context, R.color.colorPrimaryDark)) .setChannelId(CHANNEL_ID) .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setContentIntent(contentIntent) //.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE) .setPriority(NotificationManager.IMPORTANCE_HIGH); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ALARM) .build(); String Description = msg; int importance = NotificationManager.IMPORTANCE_HIGH; NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance); mChannel.setDescription(Description); mChannel.enableLights(true); mChannel.setLightColor(Color.RED); mChannel.enableVibration(true); mChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), audioAttributes); mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400}); mChannel.setShowBadge(false); notificationManager.createNotificationChannel(mChannel); } notificationManager.notify(0, builder.build()); } }
I have posted the whole code to make it easier to understand. I will trying to explain these whole code very shortly. You can skip this explanation if you want.
In onReceive
method we can get the details data of the incoming sms like sms body and sender address through intent
. Then I checked if the number is from blocked number or not. I have used greendao database in this app to store the blocked/spam number.
Of course you can use your own database system. Now if the number is whitelisted we show the notification with pending intent which is connected to the sms details activity. But before that we store the sms in sms provider to put it in inbox.
Or if its blocked then we show the blocked sms notification and insert/update the database entry. That’s all. So simple so quick. Take time and go through the whole code so that you can understand and please comment below if you have any difficulties.
Read SMS inbox
Now it’s time to get the all sms from the inbox and show it in recyclerview. Go ahead and create your HomeActivity or MainActivity. In this activity we will show the list of the inbox. Here we will see the code about getting data from inbox. Show this data in the recyclerview it’s on your own. If you still want to get the full code please email me.
public ist<SMSModelsDto> getSMS() { Uri uriSMSURI = Uri.parse("content://sms/"); ContentResolver cr = getContentResolver(); c = cr.query(uriSMSURI, null, null, null, null); startManagingCursor(c); if (c.moveToFirst()) { for (int i = 0; i < c.getCount(); i++) { String header = c.getString(c.getColumnIndexOrThrow("address")); if (stringSMSModelsDtoHashMap.containsKey(header)) { smsModelsDto = stringSMSModelsDtoHashMap.get(header); smsModelsDto.setRepeatCount(smsModelsDto.getRepeatCount() + 1); stringSMSModelsDtoHashMap.put(header, smsModelsDto); } else { smsModelsDto = new SMSModelsDto(); smsModelsDto.setId(c.getString(c.getColumnIndexOrThrow("_id"))); String threadId = null; threadId = c.getString(c.getColumnIndexOrThrow("thread_id"));; if(threadId != null){ smsModelsDto.setThreadId(threadId); } smsModelsDto.setHeader(header); smsModelsDto.setName(header); smsModelsDto.setBody(c.getString(c.getColumnIndexOrThrow("body"))); smsModelsDto.setRead(Byte.parseByte(c.getString(c.getColumnIndex("read")))); smsModelsDto.setDate(Long.parseLong(c.getString(c.getColumnIndexOrThrow("date")))); if (c.getString(c.getColumnIndexOrThrow("type")).contains("1")) { smsModelsDto.setType((byte) 1); } else { smsModelsDto.setType((byte) 0); } stringSMSModelsDtoHashMap.put(header, smsModelsDto); } if (sms.contains(smsModelsDto)) { sms.set(sms.indexOf(smsModelsDto), smsModelsDto); } else { sms.add(smsModelsDto); } runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); } }); c.moveToNext(); } if (PermissionCheck.readAndWriteContacts(HomeActivity.this)) { loadContactsData(); } } return sms; } public class SMSModelsDto{ private String id; private String threadId; private String header; private String body; private long date; private String name; private String image; private byte type; private byte read; private int repeatCount = 1; private boolean supportReply = false; public String getThreadId() { return threadId; } public void setThreadId(String threadId) { this.threadId = threadId; } public boolean isSupportReply() { return supportReply; } public void setSupportReply(boolean supportReply) { this.supportReply = supportReply; } public String getHeader(){ return header; } public void setHeader(String header){ this.header = header; } public String getBody(){ return body; } public void setBody(String body){ this.body = body; } public long getDate() { return date; } public void setDate(long date) { this.date = date; } public byte getType(){ return type; } public void setType(byte type){ this.type = type; } public String getId(){ return id; } public void setId(String id){ this.id = id; } public byte getRead(){ return read; } public void setRead(byte read){ this.read = read; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public int getRepeatCount(){ return repeatCount; } public void setRepeatCount(int repeatCount){ this.repeatCount = repeatCount; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString(){ return "SMSModelsDto{" + "id='" + id + '\'' + ", header='" + header + '\'' + ", body='" + body + '\'' + ", date='" + date + '\'' + ", image=" + image + ", type=" + type + ", read=" + read + '}'; } }
Here after create the cursor we are taking only 7 column data. Also we have to check the duplicate to prevent the insertion of the same sender multiple time. That’s why we used the hashmap.
Your app should be look like this
Till now so far so good. Remember when user click on the recyclerview item we have to show the message history of the particular sender in another activity. And also we have to handle the incoming intent containing the data of the sms if any other app wants to send the sms or share the text. We have to implements all of these. So in next part we will discuss about it. Till then stay tuned. Thank you.