/*
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.moez.QKSMS.model;
import android.content.Context;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu_alt.PduBody;
import com.google.android.mms.pdu_alt.PduPart;
import com.moez.QKSMS.LogTag;
import com.moez.QKSMS.MmsConfig;
import org.json.JSONArray;
import org.json.JSONObject;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.w3c.dom.smil.SMILRegionMediaElement;
import org.w3c.dom.smil.Time;
import org.w3c.dom.smil.TimeList;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
public class MediaModelFactory {
private static final String TAG = "Mms:media";
private static final boolean LOCAL_LOGV = false;
/**
* Returns the media model for the given SMILMediaElement in the PduBody.
*
* @param context Context
* @param sme The SMILMediaElement to find
* @param srcs String array of sources
* @param layouts LayoutModel
* @param pb PduBuddy
* @return MediaModel
* @throws IOException
* @throws IllegalArgumentException
* @throws MmsException
*/
public static MediaModel getMediaModel(Context context, SMILMediaElement sme,
ArrayList<String> srcs, LayoutModel layouts,
PduBody pb)
throws IOException, IllegalArgumentException, MmsException {
String tag = sme.getTagName();
String src = sme.getSrc();
PduPart part = findPart(context, pb, src, srcs);
if (sme instanceof SMILRegionMediaElement) {
return getRegionMediaModel(
context, tag, src, (SMILRegionMediaElement) sme, layouts, part);
} else {
return getGenericMediaModel(
context, tag, src, sme, part, null);
}
}
/**
* This method is meant to identify the part in the given PduBody that corresponds to the given
* src string.
*
* Essentially, a SMIL MMS is formatted as follows:
*
* 1. A smil/application part, which contains XML-like formatting for images, text, audio,
* slideshows, videos, etc.
* 2. One or more parts that correspond to one of the elements that was mentioned in the
* formatting above.
*
* In the smil/application part, elements are identified by a "src" attribute in an XML-like
* element. The challenge of this method lies in the fact that sometimes, the src string isn't
* located at all in the part that it is meant to identify.
*
* We employ several methods of pairing src strings up to parts, using certain patterns we've
* seen in failed MMS messages. These are described in this method.
* TODO TODO TODO: Create a testing suite for this!
*/
private static PduPart findPart(final Context context, PduBody pb, String src,
ArrayList<String> srcs) {
PduPart result = null;
if (src != null) {
src = unescapeXML(src);
// Sometimes, src takes the form of "cid:[NUMBER]".
if (contentIdSrc(src)) {
// Extract the content ID, and try finding the part using that.
result = pb.getPartByContentId("<" + src.substring("cid:".length()) + ">");
if (result == null) {
// Another thing that can happen is that there is a slideshow of images, each with
// "cid:[NUMBER]" src, but the parts aren't labelled with the content ID. If
// this is the case, then we just return the ith image part, where i is the position
// of src if all the srcs are sorted in ascending order as content IDs. srcs may
// include duplicates; we remove those and then identify i when the list is free from
// duplicates.
//
// i.e., for srcs = [ "cid:755", "cid:755", "cid:756", "cid:757", "cid:758" ],
// the i of "cid:755" is 0; for "cid:756" is 1, etc.
// First check that all the src strings are content IDs.
boolean allContentIDs = true;
for (String _src : srcs) {
if (!contentIdSrc(_src)) {
allContentIDs = false;
break;
}
}
if (allContentIDs) {
// Now, build a list of long IDs, sort them, and remove the duplicates.
ArrayList<Long> cids = new ArrayList<>();
for (String _src : srcs) {
cids.add(getContentId(_src));
}
Collections.sort(cids);
int removed = 0;
long previous = -1;
for (int i = 0; i < cids.size() - removed; i++) {
long cid = cids.get(i);
if (cid == previous) {
cids.remove(i);
removed++;
} else {
previous = cid;
}
}
// Find the i such that getContentId(src) == cids[i]
long cid = getContentId(src);
int i = cids.indexOf(cid);
// Finally, since the SMIL formatted part will come first, we expect to see
// 1 + cids.size() parts, and the right part for this particular cid will be
// 1 + i.
if (1 + i < pb.getPartsNum()) {
result = pb.getPart(i + 1);
}
}
}
} else if (textSrc(src)) {
// This is just a text src, so look for the PduPart that is has the "text/plain"
// content type.
for (int i = 0; i < pb.getPartsNum(); i++) {
PduPart part = pb.getPart(i);
String contentType = byteArrayToString(part.getContentType());
if ("text/plain".equals(contentType)) {
result = part;
break;
}
}
}
// Try a few more things in case the previous processing didn't work correctly:
// Search by name
if (result == null) {
result = pb.getPartByName(src);
}
// Search by filename
if (result == null) {
result = pb.getPartByFileName(src);
}
// Search by content location
if (result == null) {
result = pb.getPartByContentLocation(src);
}
// Try treating the src string as a content ID, and searching by content ID.
if (result == null) {
result = pb.getPartByContentId("<" + src + ">");
}
// TODO:
// four remaining cases currently in Firebase:
// 1. src: "image:[NUMBER]" (broken formatting)
// 2. src: "[NUMBER]" (broken formatting)
// 3. src: "[name].vcf" (we don't support x-vcard)
// 3. src: "Current Location.loc.vcf" (we don't support x-vcard)
}
if (result != null) {
return result;
}
if (pb.getPartsNum() > 0) {
final JSONArray array = new JSONArray();
for (int i = 0; i < pb.getPartsNum(); i++) {
JSONObject object = new JSONObject();
try {
object.put("part_number", i);
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("location", i);
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("charset", pb.getPart(i).getCharset());
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("content_disposition", byteArrayToString(pb.getPart(i).getContentDisposition()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("content_id", byteArrayToString(pb.getPart(i).getContentId()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("content_location", byteArrayToString(pb.getPart(i).getContentLocation()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("content_transfer_encoding", byteArrayToString(pb.getPart(i).getContentTransferEncoding()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("content_type", byteArrayToString(pb.getPart(i).getContentType()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("data", byteArrayToString(pb.getPart(i).getData()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("data_uri", pb.getPart(i).getDataUri());
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("file_name", byteArrayToString(pb.getPart(i).getFilename()));
} catch (Exception e) {
e.printStackTrace();
}
try {
object.put("name", byteArrayToString(pb.getPart(i).getName()));
} catch (Exception e) {
e.printStackTrace();
}
if (pb.getPart(i).generateLocation() != null) {
Log.d(TAG, "Location: " + pb.getPart(i).generateLocation());
if (pb.getPart(i).generateLocation().contains(src)) {
return pb.getPart(i);
}
}
array.put(object);
}
}
throw new IllegalArgumentException("No part found for the model.");
}
private static boolean textSrc(String src) {
return src != null && src.startsWith("text") && src.endsWith(".txt");
}
private static boolean contentIdSrc(String src) {
return src != null && src.startsWith("cid:");
}
private static long getContentId(String src) {
if (src == null) {
return -1;
} else {
if (LOCAL_LOGV) {
Log.v(TAG, "Initial contentId: " + src);
}
src = src.substring("cid:".length());
src = unescapeXML(src);
// Strip any leading < or trailing > ... they are present sometimes and causing error(s)
src = src.replaceAll("(^\\<)|(\\>$)", "");
if (LOCAL_LOGV) {
Log.v(TAG, "Final contentId: " + src);
}
return Long.parseLong(src);
}
}
private static String byteArrayToString(byte[] bytes) {
if (bytes != null) {
return new String(bytes);
}
return "null";
}
private static boolean hasBeenPosted(Context context, JSONArray array) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(array.toString().getBytes());
String encryptedString = new String(messageDigest.digest());
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(encryptedString, false);
}
private static void savePostStatus(Context context, JSONArray array, boolean posted) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(array.toString().getBytes());
String encryptedString = new String(messageDigest.digest());
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(encryptedString, true);
}
private static String unescapeXML(String str) {
return str.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll(""", "\"")
.replaceAll("'", "'")
.replaceAll("&", "&");
}
private static MediaModel getRegionMediaModel(Context context,
String tag, String src, SMILRegionMediaElement srme,
LayoutModel layouts, PduPart part) throws IOException, MmsException {
SMILRegionElement sre = srme.getRegion();
if (sre != null) {
RegionModel region = layouts.findRegionById(sre.getId());
if (region != null) {
return getGenericMediaModel(context, tag, src, srme, part, region);
}
} else {
String rId;
if (tag.equals(SmilHelper.ELEMENT_TAG_TEXT)) {
rId = LayoutModel.TEXT_REGION_ID;
} else {
rId = LayoutModel.IMAGE_REGION_ID;
}
RegionModel region = layouts.findRegionById(rId);
if (region != null) {
return getGenericMediaModel(context, tag, src, srme, part, region);
}
}
throw new IllegalArgumentException("Region not found or bad region ID.");
}
// When we encounter a content type we can't handle, such as "application/vnd.smaf", instead
// of throwing an exception and crashing, insert an empty TextModel in its place.
private static MediaModel createEmptyTextModel(Context context, RegionModel regionModel)
throws IOException {
return new TextModel(context, ContentType.TEXT_PLAIN, null, regionModel);
}
private static MediaModel getGenericMediaModel(Context context,
String tag, String src, SMILMediaElement sme, PduPart part,
RegionModel regionModel) throws IOException, MmsException {
byte[] bytes = part.getContentType();
if (bytes == null) {
throw new IllegalArgumentException(
"Content-Type of the part may not be null.");
}
String contentType = new String(bytes);
MediaModel media;
switch (tag) {
case SmilHelper.ELEMENT_TAG_TEXT:
media = new TextModel(context, contentType, src,
part.getCharset(), part.getData(), regionModel);
break;
case SmilHelper.ELEMENT_TAG_IMAGE:
media = new ImageModel(context, contentType, src,
part.getDataUri(), regionModel);
break;
case SmilHelper.ELEMENT_TAG_VIDEO:
media = new VideoModel(context, contentType, src,
part.getDataUri(), regionModel);
break;
case SmilHelper.ELEMENT_TAG_AUDIO:
media = new AudioModel(context, contentType, src,
part.getDataUri());
break;
case SmilHelper.ELEMENT_TAG_REF:
if (ContentType.isTextType(contentType)) {
media = new TextModel(context, contentType, src,
part.getCharset(), part.getData(), regionModel);
} else if (ContentType.isImageType(contentType)) {
media = new ImageModel(context, contentType, src,
part.getDataUri(), regionModel);
} else if (ContentType.isVideoType(contentType)) {
media = new VideoModel(context, contentType, src,
part.getDataUri(), regionModel);
} else if (ContentType.isAudioType(contentType)) {
media = new AudioModel(context, contentType, src,
part.getDataUri());
} else {
Log.d(TAG, "[MediaModelFactory] getGenericMediaModel Unsupported Content-Type: "
+ contentType);
media = createEmptyTextModel(context, regionModel);
}
break;
default:
throw new IllegalArgumentException("Unsupported TAG: " + tag);
}
// Set 'begin' property.
int begin = 0;
TimeList tl = sme.getBegin();
if ((tl != null) && (tl.getLength() > 0)) {
// We only support a single begin value.
Time t = tl.item(0);
begin = (int) (t.getResolvedOffset() * 1000);
}
media.setBegin(begin);
// Set 'duration' property.
int duration = (int) (sme.getDur() * 1000);
if (duration <= 0) {
tl = sme.getEnd();
if ((tl != null) && (tl.getLength() > 0)) {
// We only support a single end value.
Time t = tl.item(0);
if (t.getTimeType() != Time.SMIL_TIME_INDEFINITE) {
duration = (int) (t.getResolvedOffset() * 1000) - begin;
if (duration == 0 && (media instanceof AudioModel || media instanceof VideoModel)) {
duration = MmsConfig.getMinimumSlideElementDuration();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "[MediaModelFactory] compute new duration for " + tag + ", duration=" + duration);
}
}
}
}
}
media.setDuration(duration);
if (!MmsConfig.getSlideDurationEnabled()) {
/**
* Because The slide duration is not supported by mmsc,
* the device has to set fill type as FILL_FREEZE.
* If not, the media will disappear while rotating the screen
* in the slide show play view.
*/
media.setFill(SMILMediaElement.FILL_FREEZE);
} else {
// Set 'fill' property.
media.setFill(sme.getFill());
}
return media;
}
}