Receive MMS messages in Android KitKat
Asked Answered
P

1

16

So this video Android 4.4 SMS APIs from #DevBytes explains the recent changes to the SMS APIs in KitKat. They also provide a link with a sample project. http://goo.gl/uQ3Nih

They suggest that you handle the receive of an MMS in a service. Which all looks fine, except they neglect to mention the most undocumented piece. How to actually handle an incoming MMS.

Here is the sample from the project https://gist.github.com/lawloretienne/8970938

I have tried to "handle the MMS"

https://gist.github.com/lawloretienne/8971050

I can get the extras from the intent but the only meaningful thing that I can extract is the number from which the MMS was sent.

Can anyone point me in the right direction about how to go about this?

I noticed that a WAP_PUSH_MESSAGE contains a few things, a FROM, SUBJECT, and CONTENT_LOCATION.

The content location appears to be the url where the content of the MMS is contained. How can I access this?

Here is an example of that URL

https://atl1mmsget.msg.eng.t-mobile.com/mms/wapenc?location=XXXXXXXXXXX_14zbwk&rid=027

Where the X is a digit in the phone number of the device I am testing on.

It looks like the MMSC (Multimedia Messaging Service Center) for T-Mobile in the U.S. is http://mms.msg.eng.t-mobile.com/mms/wapenc

According to this list : http://www.activexperts.com/xmstoolkit/mmsclist/

Processional answered 13/2, 2014 at 7:53 Comment(4)
How can you offer a bounty of 300 if your reputation is 158? uoy need at least another 142 reputation points. Am I missing something?Gabey
I previously had 458 Reputation, and am willing to offer a large bounty because I have not come across any solutions to this problem for a long time.Processional
I see... The points are taken BEFORE you assign the bounty. Sorry, I have no experience with bounties, yet and it appeared very strange to me. ;)Gabey
@toobsco42 Please update your dead links. Also, did you ever get this to work? I posted a related question here.Royden
N
26

There's zero documentation so here's some info to help.

1) com.google.android.mms.pdu from source. You need the Pdu utils.

2) You get the notification push from byte array extra of the incoming mms broadcast (intent.getByteArrayExtra("data")).

3) Parse the notification push into a GenericPdu (new PduParser(rawPdu).parse()).

4) You'll need TransactionSettings to communicate with the carrier's wap server. I get the transaction settings after #5 below. I use:

TransactionSettings transactionSettings = new TransactionSettings(mContext, mConnMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS).getExtraInfo());

5) Force network comm over wifi. I use the following.

private boolean beginMmsConnectivity() {
    try {
        int result = mConnMgr.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, Phone.FEATURE_ENABLE_MMS);
        NetworkInfo info = mConnMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
        boolean isAvailable = info != null && info.isConnected() && result == Phone.APN_ALREADY_ACTIVE && !Phone.REASON_VOICE_CALL_ENDED.equals(info.getReason());
        return isAvailable;
    } catch(Exception e) {
        return false;
    }
}

6) You then need to ensure a route to the host.

private static void ensureRouteToHost(ConnectivityManager cm, String url, TransactionSettings settings) throws IOException {
    int inetAddr;
    if (settings.isProxySet()) {
        String proxyAddr = settings.getProxyAddress();
        inetAddr = lookupHost(proxyAddr);
        if (inetAddr == -1) {
            throw new IOException("Cannot establish route for " + url + ": Unknown host");
        } else {
            if (!cm.requestRouteToHost(ConnectivityManager.TYPE_MOBILE_MMS, inetAddr))
                throw new IOException("Cannot establish route to proxy " + inetAddr);
        }
    } else {
        Uri uri = Uri.parse(url);
        inetAddr = lookupHost(uri.getHost());
        if (inetAddr == -1) {
            throw new IOException("Cannot establish route for " + url + ": Unknown host");
        } else {
            if (!cm.requestRouteToHost(ConnectivityManager.TYPE_MOBILE_MMS, inetAddr))
                throw new IOException("Cannot establish route to " + inetAddr + " for " + url);
        }
    }
}

Here's the lookupHost method:

private static int lookupHost(String hostname) {
    InetAddress inetAddress;
    try {
        inetAddress = InetAddress.getByName(hostname);
    } catch (UnknownHostException e) {
        return -1;
    }
    byte[] addrBytes;
    int addr;
    addrBytes = inetAddress.getAddress();
    addr = ((addrBytes[3] & 0xff) << 24) | ((addrBytes[2] & 0xff) << 16) | ((addrBytes[1] & 0xff) << 8) | (addrBytes[0] & 0xff);
    return addr;
}

I also like to use a reflection based method for improved ensureRouteToHost functionality:

private static void ensureRouteToHostFancy(ConnectivityManager cm, String url, TransactionSettings settings) throws IOException, NoSuchMethodException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    Method m = cm.getClass().getMethod("requestRouteToHostAddress", new Class[] { int.class, InetAddress.class });
    InetAddress inetAddr;
    if (settings.isProxySet()) {
        String proxyAddr = settings.getProxyAddress();
        try {
            inetAddr = InetAddress.getByName(proxyAddr);
        } catch (UnknownHostException e) {
            throw new IOException("Cannot establish route for " + url + ": Unknown proxy " + proxyAddr);
        }
        if (!(Boolean) m.invoke(cm, new Object[] { ConnectivityManager.TYPE_MOBILE_MMS, inetAddr }))
            throw new IOException("Cannot establish route to proxy " + inetAddr);
    } else {
        Uri uri = Uri.parse(url);
        try {
            inetAddr = InetAddress.getByName(uri.getHost());
        } catch (UnknownHostException e) {
            throw new IOException("Cannot establish route for " + url + ": Unknown host");
        }
        if (!(Boolean) m.invoke(cm, new Object[] { ConnectivityManager.TYPE_MOBILE_MMS, inetAddr }))
            throw new IOException("Cannot establish route to " + inetAddr + " for " + url);
    }
}

7) After ensuring a route to the host you can then need HttpUtls from source. I've heavily modified my implementation using OkHttp for improved communications.

byte[] rawPdu = HttpUtils.httpConnection(mContext, mContentLocation, null, HttpUtils.HTTP_GET_METHOD, mTransactionSettings.isProxySet(), mTransactionSettings.getProxyAddress(), mTransactionSettings.getProxyPort());

8) From the resulting byte array use the PduParser to parge the GenericPdu. Then you can extract the body and cast to a MultimediaMessagePdu.

9) Then you can iterate the parts of the PDU.

There are countless things to consider with MMS. One thing that comes to mind is how annoying Slideshows are, so what I do is detect if there are more than 1 parts in the PDU, then I copy the headers and create separate MultimediaMessagePdu of which I save them to the phone's mms content provider separately. Don't forget to copy the headers especially if you are supporting group messaging. Group messaging is another story because the incomging telephone number in the PDU doesn't tell the whole story (MultimediaMessagePdu.mmpdu()). There's more contacts in the header that you extract using the following code.

private HashSet<String> getRecipients(GenericPdu pdu) {
    PduHeaders header = pdu.getPduHeaders();
    HashMap<Integer, EncodedStringValue[]> addressMap = new HashMap<Integer, EncodedStringValue[]>(ADDRESS_FIELDS.length);
    for (int addrType : ADDRESS_FIELDS) {
        EncodedStringValue[] array = null;
        if (addrType == PduHeaders.FROM) {
            EncodedStringValue v = header.getEncodedStringValue(addrType);
            if (v != null) {
                array = new EncodedStringValue[1];
                array[0] = v;
            }
        } else {
            array = header.getEncodedStringValues(addrType);
        }
        addressMap.put(addrType, array);
    }
    HashSet<String> recipients = new HashSet<String>();
    loadRecipients(PduHeaders.FROM, recipients, addressMap, false);
    loadRecipients(PduHeaders.TO, recipients, addressMap, true);
    return recipients;
}

Here's the load recipients method:

private void loadRecipients(int addressType, HashSet<String> recipients, HashMap<Integer, EncodedStringValue[]> addressMap, boolean excludeMyNumber) {
    EncodedStringValue[] array = addressMap.get(addressType);
    if (array == null) {
        return;
    }
    // If the TO recipients is only a single address, then we can skip loadRecipients when
    // we're excluding our own number because we know that address is our own.
    if (excludeMyNumber && array.length == 1) {
        return;
    }
    String myNumber = excludeMyNumber ? mTelephonyManager.getLine1Number() : null;
    for (EncodedStringValue v : array) {
        if (v != null) {
            String number = v.getString();
            if ((myNumber == null || !PhoneNumberUtils.compare(number, myNumber)) && !recipients.contains(number)) {
                // Only add numbers which aren't my own number.
                recipients.add(number);
            }
        }
    }
}

Here's how to iterate the MultimediaMessagePdu parts.

private void processPduAttachments() throws Exception {
    if (mGenericPdu instanceof MultimediaMessagePdu) {
        PduBody body = ((MultimediaMessagePdu) mGenericPdu).getBody();
        if (body != null) {
            int partsNum = body.getPartsNum();
            for (int i = 0; i < partsNum; i++) {
                try {
                    PduPart part = body.getPart(i);
                    if (part == null || part.getData() == null || part.getContentType() == null || part.getName() == null)
                        continue;
                    String partType = new String(part.getContentType());
                    String partName = new String(part.getName());
                    Log.d("Part Name: " + partName);
                    Log.d("Part Type: " + partType);
                    if (ContentType.isTextType(partType)) {
                    } else if (ContentType.isImageType(partType)) {
                    } else if (ContentType.isVideoType(partType)) {
                    } else if (ContentType.isAudioType(partType)) {
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    // Bad part shouldn't ruin the party for the other parts
                }
            }
        }
    } else {
        Log.d("Not a MultimediaMessagePdu PDU");
    }
}

There's many more considerations such as animated GIF support, which is entirely possible :) Some carriers support acknowledge reports, and delivery reports too, you can most likely neglect these wap communications unless a user really really wants mms delivery reports.

Nazarene answered 22/2, 2014 at 21:50 Comment(4)
Hey @Noah , there are tons of classes in the android source code that have to do with mms. is there a specific set of classes that I will need to get this working? Because there are tons of dependencies to resolve with invalid imports.Processional
Primarily you need the pdu utilities. com.google.android.mms.pdu. You'll need TransactionSettings and its dependencies, it has some issues and needs to be heavily simplified in many regards, at least the projection and where clause of the content provider query. Keep in mind the query in transaction settings will only work reliably on KitKat devices.Nazarene
I am not understanding the use of the constants like ADDRESS_FIELDS.length can you clarify what these are supposed to be initialized to?Collate
@TimMiller They are values imported from one of the source files. private static final int[] ADDRESS_FIELDS = new int[] { PduHeaders.BCC, PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO };Interdental

© 2022 - 2024 — McMap. All rights reserved.