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

1

9

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):

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:

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.

Christopher answered 29/12, 2014 at 23:2 Comment(1)
Shouldn't the sent folder reflect only messages that have been sent (or at least attempted), and not "fake" ones that haven't?Leibowitz
D
17

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.

Deteriorate answered 30/12, 2014 at 17:15 Comment(10)
This worked perfectly! I tested on an Android 4.4.4 device that I also have here. I will try to run this over the Lollipop release in the next days. Thanks!Christopher
Still not able to delete smsFerrosilicon
@Ferrosilicon Did you remember the WRITE_SMS permission? Can you insert a message? What are the returned values for the public static methods? Does your delete code work on a version prior to KitKat? Which device and Android version are you testing on? I have a limited testbed, but everything works for me, and, apparently, the OP.Deteriorate
@Ferrosilicon If you have a specific environment in which this does not work, please let me know the details, and I'll add it as a caveat to the answer.Deteriorate
I will asap! (canWriteSms was true)Ferrosilicon
This is a fantastic piece of code Mike. It worked superbly on KitKat. However on my Lollipop Android version 5.0.1 I get a "does not have android.permission.UPDATE_APP_OPS_STATS" exception. Do you have any ideas on how to solve that issue? Many thanksMilewski
@AndyWeinstein Cool, but that's a bummer about Lollipop. I figured they might've closed this hole already, which is why I added the note. I've not done anything at all with Lollipop yet, let alone gone digging through the source, but leave your comment here, and I'll let you know if I ever figure out another way to do this.Deteriorate
@MikeM. I think, you are correct. I am also getting the same "does not have android.permission.UPDATE_APP_OPS_STATS" error in version 5.1.1. I tried adding above permission but thats also of no use as there is no permission like this?. Is there any other way ?Topdress
@Topdress Nope. This only works in 4.4. After that, there's pretty much no way to get around the Provider's default app restriction.Deteriorate
@MikeM. Thanks man for you post and Bharat please add this permission(<uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS"/>) in manifest file but it only work for system apps, if you push your apk in device at directory /system/priv-app/ then this code will work perfectly and i am now able to delete SMS even in Android-M without asking runtime permission from user and i am creating app which is intended as system app so it works perfectly for me. Thank you so muchIdealistic

© 2022 - 2024 — McMap. All rights reserved.