4

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;
                }
            }
        }
    }
}
Shane Sepac
  • 806
  • 10
  • 20

2 Answers2

1

I added following lines after message is declared and it seems to work.

message.setFromAddress(Utils.getMyPhoneNumber(mContext));
message.setSave(false);
Uri uri = Uri.parse("content://mms-sms/conversations/");
message.setMessageUri(uri);

I'm not sure what the fork is doing but I suspect it is not using the system messaging libraries but using the Klinker libraries to send the message. That's what's allowing it to send messages without setting the app as default.

To get it to work without system libraries, you need to setSave as false. Then you need to provide a URI for it to work. The FromAddress may or may not be needed.

saanity
  • 23
  • 5
0

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.

Shane Sepac
  • 806
  • 10
  • 20
  • Did your ever get your OP issue resolved? What you say above re: Play is kinda unrelated to the question you asked. – PVS Mar 10 '21 at 01:24
  • @PVS I did get it resolved but it wasn't the solution I wanted, sorry if that's confusing. I was able to send SMS through the app using the native API. I was also able to send MMS, however, it only worked when my app was set as the default messaging app (this is going to be unacceptable to a lot of people). I mentioned the part about Google Play store because it was an additional component I felt was relevant so people didn't wind up on the same road I did... – Shane Sepac Mar 10 '21 at 04:26
  • 1
    Thank you for the update. The Play Store approval process for apps requiring SMS/MMS related permissions is quite cumbersome and capricious. And with RCS rolling out fast, the problems for devs in this area are never-ending. – PVS Mar 10 '21 at 15:02