Search code examples
javaandroidsmsmms

Android- Sending MMS Programmatically Without Being Default App


The MMS portion of the Android SDK is largely not exposed, and consequently there is really one main 3rd party library that helps out with this. The sample project included in the library will not send MMS, however, a fork of the project does.

When I download the latter project, I can run the sample and send an MMS without having to set the application as the default application. However, when I integrate the code into my own application MMS will only send if I make my app the default messaging app. I have read that Android 4.4+ does not support MMS without being a default app, but why is it that the sample project I'm deriving my code from works without being default? The only difference I really notice is that the minimum SDK is 14 versus my project which is 18.

Here is the main code from my project:

Manifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.hacknow2">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.READ_SMS" />
    <uses-permission android:name="android.permission.WRITE_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_MMS" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:name=".init.App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".phase1.PermissionsActivity"/>

        <activity android:name=".phase2.youtube.YoutubeActivity" />

        <activity android:name=".phase2.posting.PostingActivity" />
        <activity
            android:name=".phase2.ui.main.SelectionActivity"
            android:label="@string/title_activity_selection"
            android:theme="@style/AppTheme.NoActionBar">
            <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>
        <activity android:name=".phase1.OpeningActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver
            android:name=".phase3.mms.KitSmsSentReceiver"
            android:permission="android.permission.BROADCAST_SMS">
            <intent-filter>
                <action android:name="android.provider.Telephony.SMS_DELIVER" />
            </intent-filter>
        </receiver>

        <receiver
            android:name=".phase3.mms.KitMmsSentReceiver"
            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>

        <receiver
            android:name="com.klinker.android.send_message.MmsSentReceiver"
            android:taskAffinity="com.klinker.android.messaging.MMS_SENT" />

        <receiver
            android:name="com.klinker.android.send_message.MmsReceivedReceiver"
            android:taskAffinity="com.klinker.android.messaging.MMS_RECEIVED" />

        <service android:name="com.android.mms.transaction.TransactionService" />

        <service
            android:name=".phase3.mms.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>
    </application>

</manifest>

MMSManager, singleton that manages sending MMS

public class MMSManager {

    private static final String TAG = "MMSManager";
    private static ThreadPoolExecutor mThreadManager;
    private static MMSManager INSTANCE;
    private static BlockingQueue<Runnable> decodeWorkQueue;
    private static int NUMBER_OF_CORES =
            Runtime.getRuntime().availableProcessors();
    private Settings mSettings;
    private Context mContext;

    private MMSManager(Context c) {
        mContext = c;
        initSettings();
        initLogging();
        // A queue of Runnables
        decodeWorkQueue = new LinkedBlockingQueue<Runnable>();
        // setting the thread factory
        mThreadManager = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES,
                50, TimeUnit.MILLISECONDS, decodeWorkQueue);

        BroadcastUtils.sendExplicitBroadcast(c, new Intent(), "test action");
    }

    //See https://stackoverflow.com/questions/14057273/android-singleton-with-global-context
    private static synchronized MMSManager getSync() {
        if (INSTANCE == null) INSTANCE = new MMSManager(App.get());
        return INSTANCE;
    }

    public static MMSManager getInstance(Context c) {
        if (INSTANCE == null) {
            INSTANCE = getSync();
        }
        return INSTANCE;
    }

    private void initSettings() {
        mSettings = Settings.get(mContext);

        if (TextUtils.isEmpty(mSettings.getMmsc()) &&
                Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            initApns();
        }
    }

    private void initApns() {
        ApnUtils.initDefaultApns(mContext, new ApnUtils.OnApnFinishedListener() {
            @Override
            public void onFinished() {
                mSettings = Settings.get(mContext, true);
            }
        });
    }

    private void initLogging() {
        com.klinker.android.logger.Log.setDebug(true);
        com.klinker.android.logger.Log.setPath("messenger_log.txt");
        com.klinker.android.logger.Log.setLogListener(new OnLogListener() {
            @Override
            public void onLogged(String tag, String message) {
                //logAdapter.addItem(tag + ": " + message);
                android.util.Log.d("MMS_Manager " + tag, "onLogged: " + message);
            }
        });
    }

    //Not sure what exception might pop up but it's being handled anyway...
    public void sendMMS(String phoneNumber, Bitmap bm) {
        mThreadManager.execute(new Runnable() {
            @Override
            public void run() {
                Log.d("ThreadPool/MMSManager", "Trying to send MMS.");
                com.klinker.android.send_message.Settings sendSettings = new com.klinker.android.send_message.Settings();
                sendSettings.setMmsc(mSettings.getMmsc());
                sendSettings.setProxy(mSettings.getMmsProxy());
                sendSettings.setPort(mSettings.getMmsPort());
                sendSettings.setUseSystemSending(true);

                Transaction transaction = new Transaction(mContext, sendSettings);

                Message message = new Message(null, phoneNumber);

                if (bm != null)
                    message.setImage(bm);

                transaction.sendNewMessage(message, Transaction.NO_THREAD_ID);
            }
        });
    }
}

The fragment that calls MMSManager methods, located in its parent, SelectionActivity:

public class SendDialog extends DialogFragment {

    private static final int MY_PERMISSIONS_REQUEST_SEND_SMS = 1000;
    private ArrayList<String> mPhoneNumbers;
    private DMessage mMessage;
    private OnFinished mCompletionListener;
    private MMSManager mMMSManager;
    private Bitmap mBitmap;
    private static String[] mMediaTypes;

    public SendDialog(ArrayList<String> phoneNumbers, DMessage m, OnFinished listener){
        mPhoneNumbers = phoneNumbers;
        mMessage = m;
        mCompletionListener = listener;
    }

    public SendDialog(ArrayList<String> phoneNumbers, DMessage m, Bitmap b, OnFinished listener){
        mPhoneNumbers = phoneNumbers;
        mMessage = m;
        mCompletionListener = listener;
        mBitmap = b;
    }

    public interface OnFinished{
        void complete(boolean success);
    }

    private String getCautionMessage(){
        int numContacts = 0;
        if(mPhoneNumbers!=null && mPhoneNumbers.size()>0) {
            for (String number :
                    mPhoneNumbers) {
                numContacts++;
            }
        }
        return "Are you sure you'd like to send the highlighted post to " + numContacts + " people?";
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        mMediaTypes = getResources().getStringArray(R.array.postoptions);
        // Use the Builder class for convenient dialog construction
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage(getCautionMessage())
                .setPositiveButton(R.string.OK, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        sendBulkTexts();
                        dismiss();
                    }
                })
                .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        mCompletionListener.complete(false);
                    }
                });
        // Create the AlertDialog object and return it
        return builder.create();
    }

    private boolean isMMS(){
        return mMessage.getMediaType().equals(mMediaTypes[2]);//mediaTypes[2] == "Image"
    }

    /**
     * Converts phone number to only numbers.
     * I.e. (XXX)-XXX-XXXX to XXXXXXXXXX
     */
    private static String phoneNumberFormatter(String unformatted) {
        return unformatted.replaceAll("\\D+", "");
    }

    private void sendBulkTexts(){

        for (String number :
                mPhoneNumbers) {
            try {
                String formattedNumber = phoneNumberFormatter(number);
                if(!isMMS()) {
                    //Send a Youtube video or text
                    sendSMS(formattedNumber, mMessage.getBody());
                }
                else{
                    if(mMMSManager==null)//instantiate MMSManager and load bitmap
                        mMMSManager = MMSManager.getInstance(getActivity());
                    if(mBitmap!=null)
                        mMMSManager.sendMMS(formattedNumber, mBitmap);
                }
            }
            catch(Exception e){
                Toast.makeText(getActivity(), "Could not send text to " + number + ".", Toast.LENGTH_LONG).show();
                Log.e("SendDialog", "sendBulkTexts: ", e);
                mCompletionListener.complete(false);
            }
        }
        Toast.makeText(getActivity(), "Finished sending texts.", Toast.LENGTH_LONG).show();
        mCompletionListener.complete(true);
    }

    private void sendSMS(String phoneNumber, String message) throws Exception{
        SmsManager smsManager = SmsManager.getDefault();
        smsManager.sendTextMessage(phoneNumber, null, message, null, null);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case MY_PERMISSIONS_REQUEST_SEND_SMS: {
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    sendBulkTexts();
                } else {
                    Toast.makeText(getActivity(),
                            "Permission for sending SMSs denied.", Toast.LENGTH_LONG).show();
                    mCompletionListener.complete(false);
                    return;
                }
            }
        }
    }
}

Solution

  • I have found the answer.

    Apparently, in 2019 Google changed their rules such that you cannot add an app to the Google Play store that sends SMS/MMS directly from your app if it is not the default messaging app without some sort of strong justification. All you are allowed to do is create an intent with your SMS and MMS and pass it to the Android OS to use your default messaging app.

    My recommendation is to avoid trying to write SMS/MMS using native APIs (unless you are absolutely confident you are a rare exception to Google Play's terms) and use a third-party service like Twilio to handle this for you.