如何在Android中读取MMS数据?

79
我想读取MMS数据。我已看到mmssms.db中存储MMS条目的part表。我正在使用游标并想知道适当的URI。我正在使用"content://mms-sms/conversations"和"Address"(发送至)、"Text"或"Subject"以及图像的"Data"列名。
我已查看了mmssms.db的架构和Part表的列。

mmssms.db 数据库是固件的一部分,Android 应用程序无法访问它。content://mms-sms/conversations 内容提供程序不是 SDK 的一部分,Android 应用程序不应访问它。 - CommonsWare
我正在这里做类似的事情!http://stackoverflow.com/questions/11556633/parse-application-smil-mms-mime-type-on-android - Etienne Lawlor
5个回答

291

关于此事的文档很难找到,所以我会在这里收集所有我发现的信息。如果您时间紧迫或者不喜欢阅读,请跳转至如何获取SMS数据部分。

content://mms-sms/conversations

这是MMS和SMS提供程序的URI......它允许我们同时查询MMS和SMS数据库,并将它们混合在一个单独的线程中(称为对话)。

为什么URI很重要呢?嗯,这是获取MMS和SMS消息的标准方式;例如,当您收到短信并单击通知栏时,它将发送像这样的广播意图:content://mms-sms/conversations/XXX,其中XXX是对话的ID。

获取所有对话的列表

您唯一需要做的就是查询content://mms-sms/conversations URI:

ContentResolver contentResolver = getContentResolver();
final String[] projection = new String[]{"*"};
Uri uri = Uri.parse("content://mms-sms/conversations/");
Cursor query = contentResolver.query(uri, projection, null, null, null);

注意:通常情况下,当您调用query并希望返回所有列时,可以将null作为projection参数传递。但是,在此提供程序中无法这样做,这就是我为什么使用*的原因。

现在,您可以像往常一样循环遍历Cursor。以下是您想要使用的更重要的列:

  • _id是消息的ID。显而易见吧?其实不是。可以使用此ID使用content://smscontent://mms检索详细信息。
  • date不需要解释。
  • thread_id是对话的ID
  • body此对话中最后一个短信的内容。如果它是MMS,则即使它有文本部分,这也将是null

注意:如果您查询content://mms-sms/conversations,它将返回不同会话的列表,其中每个会话的_id是每个会话中的最后一个短信或MMS。如果您查询content://mms-sms/conversations/xxx,它将返回ID为xxx的会话中的每个SMS和/或MMS。

如何区分SMS和MMS

通常,您将希望知道正在处理的消息类型。文档说:

可以在查询的投影中请求虚拟列MmsSms.TYPE_DISCRIMINATOR_COLUMN。它的值是“mms”或“sms”,具体取决于表示行的消息是MMS消息还是SMS消息。

我认为它指的是此变量...但是我无法让它起作用。如果您已经成功,请告诉我如何操作或编辑此帖子。

到目前为止,这就是我所做的,它似乎有效,但肯定有更好的方法:

ContentResolver contentResolver = getContentResolver();
final String[] projection = new String[]{"_id", "ct_t"};
Uri uri = Uri.parse("content://mms-sms/conversations/");
Cursor query = contentResolver.query(uri, projection, null, null, null);
if (query.moveToFirst()) {
    do {
        String string = query.getString(query.getColumnIndex("ct_t"));
        if ("application/vnd.wap.multipart.related".equals(string)) {
            // it's MMS
        } else {
            // it's SMS
        }
    } while (query.moveToNext());
}

如何从短信中获取数据

如果您拥有短信的ID,则您需要做的是:

String selection = "_id = "+id;
Uri uri = Uri.parse("content://sms");
Cursor cursor = contentResolver.query(uri, null, selection, null, null);
String phone = cursor.getString(cursor.getColumnIndex("address"));
int type = cursor.getInt(cursor.getColumnIndex("type"));// 2 = sent, etc.
String date = cursor.getString(cursor.getColumnIndex("date"));
String body = cursor.getString(cursor.getColumnIndex("body"));

如何从MMS数据中获取数据?

MMS有些不同。它们可以由不同的部分(文本、音频、图像等)构成;因此,这里将介绍如何分别检索每种数据。

假设我们在mmsId变量中拥有MMS id。我们可以使用content://mms/提供程序获得关于这个MMS的详细信息:

Uri uri = Uri.parse("content://mms/");
String selection = "_id = " + mmsId;
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);

然而,唯一有趣的列是read,如果消息已被阅读,则为1

如何从MMS中获取文本内容

在这里我们必须使用content://mms/part... 例如:

String selectionPart = "mid=" + mmsId;
Uri uri = Uri.parse("content://mms/part");
Cursor cursor = getContentResolver().query(uri, null,
    selectionPart, null, null);
if (cursor.moveToFirst()) {
    do {
        String partId = cursor.getString(cursor.getColumnIndex("_id"));
        String type = cursor.getString(cursor.getColumnIndex("ct"));
        if ("text/plain".equals(type)) {
            String data = cursor.getString(cursor.getColumnIndex("_data"));
            String body;
            if (data != null) {
                // implementation of this method below
                body = getMmsText(partId);
            } else {
                body = cursor.getString(cursor.getColumnIndex("text"));
            }
        }
    } while (cursor.moveToNext());
}

它可能包含不同部分的文本...但通常只有一个。因此,如果您想要删除循环,它将在大多数情况下运行。这就是getMmsText方法的样子:

private String getMmsText(String id) {
    Uri partURI = Uri.parse("content://mms/part/" + id);
    InputStream is = null;
    StringBuilder sb = new StringBuilder();
    try {
        is = getContentResolver().openInputStream(partURI);
        if (is != null) {
            InputStreamReader isr = new InputStreamReader(is, "UTF-8");
            BufferedReader reader = new BufferedReader(isr);
            String temp = reader.readLine();
            while (temp != null) {
                sb.append(temp);
                temp = reader.readLine();
            }
        }
    } catch (IOException e) {}
    finally {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {}
        }
    }
    return sb.toString();
}

如何从MMS获取图像

与获取文本部分相同...唯一的区别是你需要查找不同的MIME类型:

String selectionPart = "mid=" + mmsId;
Uri uri = Uri.parse("content://mms/part");
Cursor cPart = getContentResolver().query(uri, null,
    selectionPart, null, null);
if (cPart.moveToFirst()) {
    do {
        String partId = cPart.getString(cPart.getColumnIndex("_id"));
        String type = cPart.getString(cPart.getColumnIndex("ct"));
        if ("image/jpeg".equals(type) || "image/bmp".equals(type) ||
                "image/gif".equals(type) || "image/jpg".equals(type) ||
                "image/png".equals(type)) {
            Bitmap bitmap = getMmsImage(partId);
        }
    } while (cPart.moveToNext());
}

这是 getMmsImage 方法的样子:

private Bitmap getMmsImage(String _id) {
    Uri partURI = Uri.parse("content://mms/part/" + _id);
    InputStream is = null;
    Bitmap bitmap = null;
    try {
        is = getContentResolver().openInputStream(partURI);
        bitmap = BitmapFactory.decodeStream(is);
    } catch (IOException e) {}
    finally {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {}
        }
    }
    return bitmap;
}

如何获取发件人地址

您需要使用content://mms/xxx/addr提供程序,其中xxx是MMS的ID:

private String getAddressNumber(int id) {
    String selectionAdd = new String("msg_id=" + id);
    String uriStr = MessageFormat.format("content://mms/{0}/addr", id);
    Uri uriAddress = Uri.parse(uriStr);
    Cursor cAdd = getContentResolver().query(uriAddress, null,
        selectionAdd, null, null);
    String name = null;
    if (cAdd.moveToFirst()) {
        do {
            String number = cAdd.getString(cAdd.getColumnIndex("address"));
            if (number != null) {
                try {
                    Long.parseLong(number.replace("-", ""));
                    name = number;
                } catch (NumberFormatException nfe) {
                    if (name == null) {
                        name = number;
                    }
                }
            }
        } while (cAdd.moveToNext());
    }
    if (cAdd != null) {
        cAdd.close();
    }
    return name;
}

最后的想法

  • 不明白为什么谷歌有那么多钱,却不支付学生或其他人来记录这个API。您必须检查源代码才能知道它如何工作,更糟糕的是,他们不会公开在数据库列中使用的常量,因此我们必须手动编写它们。
  • 对于MMS内部的其他类型数据,您可以应用上述相同的想法......只要知道mime类型就可以了。

2
关于 content://mms-sms/conversations。这个URL包含了所有的线程列表,但不包括单独的短信或彩信。因此,如果它既不是短信也不是彩信,那么了解它就没有意义了。 - Maxim
2
Justin,因为MMS以SMIL的形式存储在数据库中作为幻灯片。 - Naba
有没有一种编程方式可以使用 _id 或 threadId 来自动删除 MMS 消息? - ssk
3
content://mms-sms/conversations 在一些手机上不能使用(例如我的 Galaxy S6 就不能)。我必须使用 content://mms/ 来完成所有操作。 - KVISH
1
除非我弄错了,MessageFormat.format("content://mms/{0}/addr", id); 只适用于小于1,000的ID。难道不应该是 MessageFormat.format("content://mms/{0,number,#}/addr", id); 吗? - Nick
显示剩余29条评论

12
Christian的答案非常好。然而,获取发件人地址的方法对我没有用。Long.parseLong语句除了可能引发异常外并没有做任何事情,new String(...)呢?
在我的设备上,光标计数为2或更多。第一个通常具有“类型”137,其他通常具有“类型”151。我找不到文档说明这一点,但可以推断出137是“来自”,151是“去向”。因此,如果我按原样运行该方法,将返回最后一行,这是收件人之一,而在许多情况下只是多个收件人中的一个,并且不会抛出异常。
另外,据我所知,选择不是必需的,因为所有行都具有相同的msg_id。但是,这并没有什么坏处。
以下是可用于获取发件人地址的方法:
public static String getMMSAddress(Context context, String id) {
    String addrSelection = "type=137 AND msg_id=" + id;
    String uriStr = MessageFormat.format("content://mms/{0}/addr", id);
    Uri uriAddress = Uri.parse(uriStr);
    String[] columns = { "address" };
    Cursor cursor = context.getContentResolver().query(uriAddress, columns,
            addrSelection, null, null);
    String address = "";
    String val;
    if (cursor.moveToFirst()) {
        do {
            val = cursor.getString(cursor.getColumnIndex("address"));
            if (val != null) {
                address = val;
                // Use the first one found if more than one
                break;
            }
        } while (cursor.moveToNext());
    }
    if (cursor != null) {
        cursor.close();
    }
    // return address.replaceAll("[^0-9]", "");
    return address;
}

我并不在意是否全是数字,但如果需要的话,我在注释中提供了一种方法来消除除数字以外的所有内容。它很容易被修改为返回所有收件人。

我假设这对他起作用。如果异常发生在第一行,它看起来会给出正确的答案。


1
我不会尝试在contact_id字段上调用parseLong方法;将其视为字符串处理。实际上,在电子邮件到彩信网关的情况下,它很可能是电子邮件地址或其他内容。 - Edward Falk
5
为了澄清type常量,它们来自于PduHeaders类:0x97 / 151是PduHeaders.TO0x89 / 137是PduHeaders.FROM。其他有效的参考值包括:0x81 / 129是PduHeaders.BCC0x82 / 130是PduHeaders.CC。也请参考Telephony.Mms.Addr - zelanix

6

我刚刚遇到了这个问题,经过努力,最终解决了,并且我认为本帖可能会从我的经验中受益。

我可以在content://mms-sms/conversations/ (Telephony.Threads.CONTENT_URI)上查询并像本帖子中所描述的那样有助于获取地址和部分内容,但我发现该URI无法检索只包含MMS消息的主题-例如,具有两个以上对应方的主题。

在浏览AOSP MMS应用程序源代码后,我发现它正在使用变量进行Telephony.Threads.CONTENT_URI生成其对话列表-它正在添加参数“simple”,值为“true”。当我添加此参数时,我发现提供者将查询完全不同的表,该表确实具有所有SMS和MMS线程。

此表与常规的Telephony.Threads.CONTENT_URI不同(???);这是AOSP应用程序正在使用的投影--

public static final String[] ALL_THREADS_PROJECTION = {
    Threads._ID, Threads.DATE, Threads.MESSAGE_COUNT, Threads.RECIPIENT_IDS,
    Threads.SNIPPET, Threads.SNIPPET_CHARSET, Threads.READ, Threads.ERROR,
    Threads.HAS_ATTACHMENT
};

这里的_ID是线程的ID - 因此是Telephony.Sms.CONTENT_URI或Telephony.Mms.CONTENT_URI中的ID。
在我发现这个奇怪的细节后,事情开始变得更好了!但请注意,“simple=true”变体中的DATE列不可靠,我不得不使用最近的Sms或Mms消息中的日期。
我还应该提到的另一件事是,为了获取特定线程的正确消息列表,我必须同时查询Mms和Sms提供程序,然后将结果组合成一个列表,再按日期排序。
我验证了在Android 5.x和7.x上的行为。
希望这能帮到你。

3

我不得不进行一些修改才能使它对我起作用。

  1. When I retrieve the cursor.getString(cursor.getColumnIndex("type")) from the mms-sms/conversations content, ("content://mms-sms/conversations/") I test the value of the "type" field for null. If the variable is null - i.e.

    String otype = c.getString(c.getColumnIndex("type"));
    if(otype != null) {
        //this is an sms - handle it...
    

    the message is an SMS, else it is an MMS. For MMS's you have to test for both mime types as follows:-

    if (("application/vnd.wap.multipart.related".equalsIgnoreCase(msg_type)
        ||"application/vnd.wap.multipart.mixed".equalsIgnoreCase(msg_type))
        && !id.equalsIgnoreCase(lastMMSID)) {
             //this is a MMS - handle it...
    
  2. When you use a ContentObserver to monitor the message content for changes, it fires several notifications for the same message. I use a static variable - in my case lastMMSID - to keep track of the message.
  3. This code works well to retrieve the content of both Inbound and Outbound messages. It is important to iterate through all the records that are returned by the "content://mms/part/" uri in order to get to the content - text and/or attachments - of the MMS.
  4. The only way that I could find that works pretty well to differentiate between inbound and outbound MMS's, is to test the null status of the "m_id" field of the mms-sms/conversations content.

    String m_id = c.getString(c.getColumnIndex("m_id"));
    String mDirection = m_id == null? "OUT": "IN";
    

关于如何获取地址字段的最后一点建议。由于某种原因,地址内容不喜欢使用{" * "}参数进行查询,但是这个可以起作用:

final String[] projection = new String[] {"address", "contact_id", "charset", "type"};

如果是出站消息,需要查找的“type”将为151。对于入站消息,“type”将为137。一个完全功能的代码应该像这样:

private String getANumber(int id) {
    String add = "";
    final String[] projection = new String[] {"address","contact_id","charset","type"};
    final String selection = "type=137 or type=151"; // PduHeaders
    Uri.Builder builder = Uri.parse("content://mms").buildUpon();
    builder.appendPath(String.valueOf(id)).appendPath("addr");

    Cursor cursor = context.getContentResolver().query(
        builder.build(),
        projection,
        selection,
        null, null);

if (cursor.moveToFirst()) {
          do {
              String add = cursor.getString(cursor.getColumnIndex("address"));
              String type: cursor.getString(cursor.getColumnIndex("type"));
          } while(cursor.moveToNext());
      }
      // Outbound messages address type=137 and the value will be 'insert-address-token'
      // Outbound messages address type=151 and the value will be the address
      // Additional checking can be done here to return the correct address.
      return add;
}

对于在这个职位上走在我前面的所有勇士们,我由衷地感谢你们!

这段话是与IT技术无关的表达感激之情的句子。

1
上面给出的获取 getMMSAddress() 的答案不应该包含 while (cursor.moveToNext()); 循环。它应该只从游标的第一个元素中提取地址。由于某种原因,我不知道,这个游标有多个记录。第一个记录包含发送者的地址。除了第一个记录之外的游标元素包含接收者的地址。因此,代码如今返回的是接收者的地址而不是发送者的地址。
这对于破解 MMS 内容非常有帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接