Android KitKat (API 19) - How to write messages in SMS Content Provider, without sending them, from Non-Default App?

I am trying to create an Android app that writes messages in the Sent Box of the system. These messages should not be sent over the GSM network to the recipient, the idea is only to write them in the Sent Content Provider.

For now, I have this code:

Manifest File

<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>

Java Class

private final String SENT_SMS_CONTENT_PROVIDER_URI_OLDER_API_19 = "content://sms/sent";

ContentValues values = new ContentValues();
values.put("address", mNumber);
values.put("body", mMessage);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
  mContext.getContentResolver().insert(Telephony.Sms.Sent.CONTENT_URI, values);
else mContext.getContentResolver().insert(Uri.parse(SENT_SMS_CONTENT_PROVIDER_URI_OLDER_API_19), values);

For a device with an API version lower than 19, this implementation works just fine. For these older sdk versions, it is only necessary to access to the content provider defined by the uri content://sms/sent.

For the newer sdk versions, this is not working. Apparently, Android changed its way of managing the SMS module in the KitKat release. According the next article, only the default SMS application can write and update the new SMS Content Provider (android.provider.Telephony.Sms.Sent - the previous content://sms/sent is also not available):

  • http://android-developers.blogspot.pt/2013/10/getting-your-sms-apps-ready-for-kitkat.html

Considering the behavior of this app, it doesn't make sense to turn it the default SMS app. This app doesn´t need to read SMS messages from the content provider and should not send any message by SmsManager.getDefault().sendTextMessage. The only thing it should do is write some messages in the Sent Provider.

As you can understand, it is also not acceptable and practicable to request the user to change the default app to mine and then go back to the previous SMS app, each time it is necessary to write a message in the Sent (this is suggested in the "Advice for SMS backup & restore apps" section in the Android Developers Blogspot).

The next article reveals some ways to unhide the option OP_WRITE_SMS:

  • http://www.androidpolice.com/2013/12/06/non-default-sms-apps-in-kitkat-can-still-write-to-the-sms-database-using-a-switch-in-app-ops-no-root-required/

Unfortunately, the next code stopped working for Android 4.4.2:

Intent intent = new Intent();
intent.setClassName("com.android.settings", "com.android.settings.Settings");
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(":android:show_fragment", "com.android.settings.applications.AppOpsSummary");
startActivity(intent);

I am out of solutions to overcome this problem.


Solution 1:

The SmsWriteOpUtils class uses reflection to access methods of the AppOpsManager Service in order to enable/disable a non-default SMS app's write access to the SMS Provider in API Level 19 (KitKat). Once set, an app's access mode will be retained until it is reset, or the app is uninstalled.

Enabling an app's write access allows that app all of the standard methods of interaction with the SMS Provider, including insert() and delete().

Please note that this class does no API Level check, and that the WRITE_SMS permission is still required.

import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public final class SmsWriteOpUtils {
    private static final int WRITE_OP_CODE = 15;

    public static boolean isWriteEnabled(Context context) {
        int result = checkOp(context);
        return result == AppOpsManager.MODE_ALLOWED;
    }

    public static boolean setWriteEnabled(Context context, boolean enabled) {
        int mode = enabled ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
        return setMode(context, mode);
    }

    private static int checkOp(Context context) {
        try {
            Method checkOpMethod = AppOpsManager.class.getMethod("checkOp",
                                                                 Integer.TYPE,
                                                                 Integer.TYPE,
                                                                 String.class);

            AppOpsManager appOpsManager =
                (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            int uid = context.getApplicationInfo().uid;
            String packageName = context.getPackageName();

            return checkOpMethod.invoke(appOpsManager, WRITE_OP_CODE, uid, packageName);
        }
        catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return -1;
    }

    private static boolean setMode(Context context, int mode) {
        try {
            Method setModeMethod = AppOpsManager.class.getMethod("setMode",
                                                                 Integer.TYPE,
                                                                 Integer.TYPE,
                                                                 String.class,
                                                                 Integer.TYPE);

            AppOpsManager appOpsManager =
                (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            int uid = context.getApplicationInfo().uid;
            String packageName = context.getPackageName();

            setModeMethod.invoke(appOpsManager, WRITE_OP_CODE, uid, packageName, mode);

            return true;
        }
        catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
    }
}

Example usage:

boolean canWriteSms;

if(!SmsWriteOpUtils.isWriteEnabled(getApplicationContext())) {
    canWriteSms = SmsWriteOpUtils.setWriteEnabled(getApplicationContext(), true);
}
...

NB: For regular user apps, this works only on API Level 19 (KitKat). The hole was patched in later versions.