/* * CDDL HEADER START * * The contents of this file are subject to the terms of the Common Development * and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at * src/com/vodafone360/people/VODAFONE.LICENSE.txt or * http://github.com/360/360-Engine-for-Android * See the License for the specific language governing permissions and * limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each file and * include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt. * If applicable, add the following below this CDDL HEADER, with the fields * enclosed by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved. * Use is subject to license terms. */ package com.vodafone360.people.engine.activities; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.text.DateFormat; import java.util.Date; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.text.TextUtils; import com.vodafone360.people.database.DatabaseHelper; import com.vodafone360.people.database.tables.ActivitiesTable.TimelineNativeTypes; import com.vodafone360.people.database.tables.ActivitiesTable.TimelineSummaryItem; import com.vodafone360.people.datatypes.ActivityItem; import com.vodafone360.people.datatypes.Contact; import com.vodafone360.people.datatypes.ContactDetail; import com.vodafone360.people.datatypes.VCardHelper; import com.vodafone360.people.service.ServiceStatus; import com.vodafone360.people.utils.LogUtils; /** * Helper class to 'decode' MMS messages to provide suitable content for * Timeline summary. */ public class MmsDecoder { private static final int ANY_CHARSET = 0x00; private static final int US_ASCII = 0x03; private static final int ISO_8859_1 = 0x04; private static final int ISO_8859_2 = 0x05; private static final int ISO_8859_3 = 0x06; private static final int ISO_8859_4 = 0x07; private static final int ISO_8859_5 = 0x08; private static final int ISO_8859_6 = 0x09; private static final int ISO_8859_7 = 0x0A; private static final int ISO_8859_8 = 0x0B; private static final int ISO_8859_9 = 0x0C; private static final int SHIFT_JIS = 0x11; private static final int UTF_8 = 0x6A; private static final int BIG5 = 0x07EA; private static final int UCS2 = 0x03E8; private static final int UTF_16 = 0x03F7; private static final String MIMENAME_ANY_CHARSET = "*"; private static final String MIMENAME_US_ASCII = "us-ascii"; private static final String MIMENAME_ISO_8859_1 = "iso-8859-1"; private static final String MIMENAME_ISO_8859_2 = "iso-8859-2"; private static final String MIMENAME_ISO_8859_3 = "iso-8859-3"; private static final String MIMENAME_ISO_8859_4 = "iso-8859-4"; private static final String MIMENAME_ISO_8859_5 = "iso-8859-5"; private static final String MIMENAME_ISO_8859_6 = "iso-8859-6"; private static final String MIMENAME_ISO_8859_7 = "iso-8859-7"; private static final String MIMENAME_ISO_8859_8 = "iso-8859-8"; private static final String MIMENAME_ISO_8859_9 = "iso-8859-9"; private static final String MIMENAME_SHIFT_JIS = "shift_JIS"; private static final String MIMENAME_UTF_8 = "utf-8"; private static final String MIMENAME_BIG5 = "big5"; private static final String MIMENAME_UCS2 = "iso-10646-ucs-2"; private static final String MIMENAME_UTF_16 = "utf-16"; protected static final Uri MMS_CONTENT_URI = Uri.parse("content://mms"); private static final int PDU_FROM_FIELD = 0x89; private static final int PDU_TO_FIELD = 0x97; private static final String THREAD_ID = "thread_id"; private static final String ID = "_id"; private static final String ELLIPSIZE = "..."; /** * Fields to be returned from MMS query. */ private static final String[] MMS_STATUS_PROJECTION = new String[] { THREAD_ID, "date", ID, "sub", "sub_cs", "msg_box", THREAD_ID }; private static final int COLUMN_DATE = 1; private static final int COLUMN_MMS_ID = 2; private static final int COLUMN_SUBJECT = 3; private static final int COLUMN_SUBJECT_CS = 4; private static final int COLUMN_MSG_BOX = 5; private static final int COLUMN_MSG_THREAD_ID = 6; private static final String[] PART_PROJECTION = new String[] { ID, "chset", "cd", "cid", "cl", "ct", "fn", "name" }; private static final int PART_COLUMN_ID = 0; private static final int PART_COLUMN_CONTENT_TYPE = 5; private static final int MESSAGE_BOX_INBOX = 1; private static final int MESSAGE_BOX_SENT = 2; private static final String MMS_SORT_ORDER = "date ASC"; private static final String PART = "part"; private static final String TEXT_PLAIN = "text/plain"; private static final int MS_IN_SECONDS = 1000; protected static long getTimestamp(Cursor mmsCursor) { return mmsCursor.getLong(COLUMN_DATE) * MS_IN_SECONDS; } /** * Get a Cursor for MMS message item from native message log. * * @param cr ContentResolver * @param timestamp Timestamp to search against (may be null) * @return Cursor to item in native message log (may be null) */ protected static Cursor fetchMmsListCursor(ContentResolver cr, boolean refresh, long oldestTimestamp, long newestTimestamp) { String whereClause = refresh ? "date > " + newestTimestamp : "date < " + oldestTimestamp; return cr.query(MmsDecoder.MMS_CONTENT_URI, MMS_STATUS_PROJECTION, whereClause, null, MMS_SORT_ORDER); } /** * Get the MMS data for the message at current Cursor position and use it to * populate a TimelineSummaryItem. We initially check if the MMS is an Inbox * or sent item returning false if this is not the case. * * @param context Context. * @param cr ContentResolver. * @param mmsCursor Cursor pointing to MMS message entry in native message * log. * @param item TimeLineSummaryItem to populate using MMS message details * @param db Handle to People database. * @param maxDescLength maximum length of the description. * @return true if we have created the TimelineSummaryItem false if we * haven't (because the MMS is not of a valid type). */ protected static boolean getMmsData(Context context, ContentResolver cr, Cursor mmsCursor, TimelineSummaryItem item, DatabaseHelper db, int maxDescLength) { int msgId = mmsCursor.getInt(COLUMN_MMS_ID); Uri msgUri = MMS_CONTENT_URI.buildUpon().appendPath(Long.toString(msgId)).build(); ActivityItem.Type type = nativeToNpMessageType(mmsCursor.getInt(COLUMN_MSG_BOX)); if (type == null) { return false; } String address = getToOrFrom(cr, type, msgUri); String sub = mmsCursor.getString(COLUMN_SUBJECT); int subcs = mmsCursor.getInt(COLUMN_SUBJECT_CS); String subject = TextUtils.isEmpty(sub) ? "" : decodeString(subcs, getMmsBytes(sub)); item.mTimestamp = mmsCursor.getLong(COLUMN_DATE) * MS_IN_SECONDS; item.mNativeItemId = msgId; item.mNativeItemType = TimelineNativeTypes.MmsLog.ordinal(); item.mType = type; item.mNativeThreadId = mmsCursor.getInt(COLUMN_MSG_THREAD_ID); item.mContactAddress = address; if (subject.length() > 0) { if (subject.length() <= maxDescLength) { item.mDescription = subject; } else { item.mDescription = subject.substring(0, maxDescLength) + ELLIPSIZE; } } else { item.mDescription = getMmsText(cr, msgId, maxDescLength); } item.mTitle = DateFormat.getDateInstance().format(new Date(item.mTimestamp)); Contact c = new Contact(); ContactDetail phoneDetail = new ContactDetail(); ServiceStatus status = db.fetchContactInfo(address, c, phoneDetail); if (ServiceStatus.SUCCESS == status) { item.mContactId = c.contactID; item.mLocalContactId = c.localContactID; item.mUserId = c.userID; item.mContactName = null; for (ContactDetail d : c.details) { switch (d.key) { case VCARD_NAME: final VCardHelper.Name name = d.getName(); if (name != null) { item.mContactName = d.getName().toString(); } break; case VCARD_IMADDRESS: item.mContactNetwork = d.alt; break; default: // do nothing. break; } } } else { item.mContactName = address; } return true; } /** * Return the MIME-type for specified character-set. * * @param charset Character set. * @return String containing MIME-type. * @throws UnsupportedEncodingException if un-supported character set * specified. */ private static String getMimeName(int charset) throws UnsupportedEncodingException { switch (charset) { case ANY_CHARSET: return MIMENAME_ANY_CHARSET; case US_ASCII: return MIMENAME_US_ASCII; case ISO_8859_1: return MIMENAME_ISO_8859_1; case ISO_8859_2: return MIMENAME_ISO_8859_2; case ISO_8859_3: return MIMENAME_ISO_8859_3; case ISO_8859_4: return MIMENAME_ISO_8859_4; case ISO_8859_5: return MIMENAME_ISO_8859_5; case ISO_8859_6: return MIMENAME_ISO_8859_6; case ISO_8859_7: return MIMENAME_ISO_8859_7; case ISO_8859_8: return MIMENAME_ISO_8859_8; case ISO_8859_9: return MIMENAME_ISO_8859_9; case SHIFT_JIS: return MIMENAME_SHIFT_JIS; case UTF_8: return MIMENAME_UTF_8; case BIG5: return MIMENAME_BIG5; case UCS2: return MIMENAME_UCS2; case UTF_16: return MIMENAME_UTF_16; default: throw new UnsupportedEncodingException(); } } /** * Decode MMS data based on character set. * * @param charset Character set. * @param data MMS data. * @return Decoded MMS data as String. */ private static String decodeString(int charset, byte[] data) { if (ANY_CHARSET == charset) { return new String(data); // system default encoding. } else { try { String name = getMimeName(charset); return new String(data, name); } catch (UnsupportedEncodingException e) { try { return new String(data, MIMENAME_ISO_8859_1); } catch (UnsupportedEncodingException ex) { return new String(data); // system default encoding. } } } } /** * Return byte array from supplied String. * * @param data String containing MMS data. * @return byte array containing data. */ private static byte[] getMmsBytes(String data) { try { return data.getBytes(MIMENAME_ISO_8859_1); } catch (UnsupportedEncodingException e) { // Impossible to reach here! LogUtils.logE("MmsDecoder.getBytes() ISO_8859_1 must be supported - " + e); return new byte[0]; } } /** * Retrieve the sender/recipient of MMS (based on whether received or sent * message). * * @param cr ContentResolver. * @param msgType ActivityItem type (i.e. MESSAGE_MMS_SENT). * @param uri MMS URI based on MMS_CONTENT_URI. * @return String containing sender/recipient (can be null). */ private static String getToOrFrom(ContentResolver cr, ActivityItem.Type msgType, Uri uri) { String msgId = uri.getLastPathSegment(); Uri.Builder builder = MMS_CONTENT_URI.buildUpon(); builder.appendPath(msgId).appendPath("addr"); int type = PDU_FROM_FIELD; if (msgType == ActivityItem.Type.MESSAGE_MMS_SENT) { type = PDU_TO_FIELD; } Uri uriPart = builder.build(); Cursor cursor = cr.query(uriPart, new String[] { "address", "charset" }, "type=" + type, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { String from = cursor.getString(0); if (!TextUtils.isEmpty(from)) { byte[] bytes = getMmsBytes(from); int charset = cursor.getInt(1); return decodeString(charset, bytes); } } } finally { cursor.close(); } } return null; } /** * Convert Native message type (Inbox, Sent) to corresponding ActivityItem * type. * * @param type Native message type. * @return ActivityItem type. */ private static ActivityItem.Type nativeToNpMessageType(int type) { switch (type) { case MESSAGE_BOX_SENT: return ActivityItem.Type.MESSAGE_MMS_SENT; case MESSAGE_BOX_INBOX: return ActivityItem.Type.MESSAGE_MMS_RECEIVED; default: // do nothing. break; } return null; } /** * Generate text required for Timeline entry from content of MMS message. * * @param cr ContentResolver * @param msgId ID of MMS message as retrieved from message log. * @param maxLength maximum length for text. * @return String containing retrieved text, can be null. */ private static String getMmsText(ContentResolver cr, long msgId, int maxLength) { String strText = null; Uri.Builder builder = MMS_CONTENT_URI.buildUpon(); builder.appendPath(Long.toString(msgId)).appendPath(PART); Cursor partsCursor = cr.query(builder.build(), PART_PROJECTION, null, null, null); while (strText == null && partsCursor.moveToNext()) { long partId = partsCursor.getLong(PART_COLUMN_ID); if (!partsCursor.isNull(PART_COLUMN_CONTENT_TYPE)) { String ct; try { ct = new String(partsCursor.getString(PART_COLUMN_CONTENT_TYPE).getBytes(), MIMENAME_ISO_8859_1); if (ct.equals(TEXT_PLAIN)) { Uri.Builder builder2 = MMS_CONTENT_URI.buildUpon(); builder2.appendPath(PART).appendPath(Long.toString(partId)); Uri partURI = builder2.build(); InputStream is = cr.openInputStream(partURI); byte[] buffer = new byte[maxLength]; int len = is.read(buffer); strText = new String(buffer, 0, len, MIMENAME_ISO_8859_1); if (len == maxLength) { strText += ELLIPSIZE; } is.close(); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } return strText; } }