/*
* 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.common.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SqliteWrapper;
import android.drm.DrmStore;
import android.media.CamcorderProfile;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.style.URLSpan;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import com.android.mms.transaction.MmsMessageSender;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu_alt.CharacterSets;
import com.google.android.mms.pdu_alt.EncodedStringValue;
import com.google.android.mms.pdu_alt.MultimediaMessagePdu;
import com.google.android.mms.pdu_alt.NotificationInd;
import com.google.android.mms.pdu_alt.PduBody;
import com.google.android.mms.pdu_alt.PduHeaders;
import com.google.android.mms.pdu_alt.PduPart;
import com.google.android.mms.pdu_alt.PduPersister;
import com.google.android.mms.pdu_alt.RetrieveConf;
import com.google.android.mms.pdu_alt.SendReq;
import com.moez.QKSMS.LogTag;
import com.moez.QKSMS.MmsConfig;
import com.moez.QKSMS.QKSMSApp;
import com.moez.QKSMS.R;
import com.moez.QKSMS.TempFileProvider;
import com.moez.QKSMS.common.google.ThumbnailManager;
import com.moez.QKSMS.common.google.UriImage;
import com.moez.QKSMS.data.Contact;
import com.moez.QKSMS.model.MediaModel;
import com.moez.QKSMS.model.SlideModel;
import com.moez.QKSMS.model.SlideshowModel;
import com.moez.QKSMS.transaction.SmsHelper;
import com.moez.QKSMS.ui.dialog.AsyncDialog;
import com.moez.QKSMS.ui.messagelist.MessageColumns;
import com.moez.QKSMS.ui.messagelist.MessageItem;
import com.moez.QKSMS.ui.mms.SlideshowActivity;
import com.moez.QKSMS.ui.popup.QKComposeActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* An utility class for managing messages.
*/
public abstract class MessageUtils {
/**
* Copies media from an Mms to the DrmProvider
*
* @param context
* @param msgId
*/
public static boolean saveRingtone(Context context, long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(context, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyToDrmProvider can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
// All parts (but there's probably only a single one) have to be successful
// for a valid result.
result &= copyPart(context, part, Long.toHexString(msgId));
}
}
return result;
}
public static boolean copyPart(Context context, PduPart part, String fallback) {
Uri uri = part.getDataUri();
String type = new String(part.getContentType());
boolean isDrm = DrmUtils.isDrmType(type);
if (isDrm) {
type = QKSMSApp.getApplication().getDrmManagerClient().getOriginalMimeType(part.getDataUri());
}
if (!ContentType.isImageType(type) && !ContentType.isVideoType(type) &&
!ContentType.isAudioType(type)) {
return true; // we only save pictures, videos, and sounds. Skip the text parts,
// the app (smil) parts, and other type that we can't handle.
// Return true to pretend that we successfully saved the part so
// the whole save process will be counted a success.
}
InputStream input = null;
FileOutputStream fout = null;
try {
input = context.getContentResolver().openInputStream(uri);
if (input instanceof FileInputStream) {
FileInputStream fin = (FileInputStream) input;
byte[] location = part.getName();
if (location == null) {
location = part.getFilename();
}
if (location == null) {
location = part.getContentLocation();
}
String fileName;
if (location == null) {
// Use fallback name.
fileName = fallback;
} else {
// For locally captured videos, fileName can end up being something like this:
// /mnt/sdcard/Android/data/com.android.mms/cache/.temp1.3gp
fileName = new String(location);
}
File originalFile = new File(fileName);
fileName = originalFile.getName(); // Strip the full path of where the "part" is
// stored down to just the leaf filename.
// Depending on the location, there may be an
// extension already on the name or not. If we've got audio, put the attachment
// in the Ringtones directory.
String dir = Environment.getExternalStorageDirectory() + "/"
+ (ContentType.isAudioType(type) ? Environment.DIRECTORY_RINGTONES :
Environment.DIRECTORY_DOWNLOADS) + "/";
String extension;
int index;
if ((index = fileName.lastIndexOf('.')) == -1) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
} else {
extension = fileName.substring(index + 1, fileName.length());
fileName = fileName.substring(0, index);
}
if (isDrm) {
extension += DrmUtils.getConvertExtension(type);
}
// Remove leading periods. The gallery ignores files starting with a period.
fileName = fileName.replaceAll("^.", "");
File file = getUniqueDestination(dir + fileName, extension);
// make sure the path is valid and directories created for this file.
File parentFile = file.getParentFile();
if (!parentFile.exists() && !parentFile.mkdirs()) {
Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
return false;
}
fout = new FileOutputStream(file);
byte[] buffer = new byte[8000];
int size = 0;
while ((size = fin.read(buffer)) != -1) {
fout.write(buffer, 0, size);
}
// Notify other applications listening to scanner events
// that a media file has been added to the sd card
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
}
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while opening or reading stream", e);
return false;
} finally {
if (null != input) {
try {
input.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
if (null != fout) {
try {
fout.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
}
return true;
}
private static File getUniqueDestination(String base, String extension) {
File file = new File(base + "." + extension);
for (int i = 2; file.exists(); i++) {
file = new File(base + "_" + i + "." + extension);
}
return file;
}
public static int getDrmMimeSavedStringRsrc(Context context, long msgId, boolean success) {
if (isDrmRingtoneWithRights(context, msgId)) {
return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
}
return 0;
}
/**
* Returns true if any part is drm'd audio with ringtone rights.
*
* @param context
* @param msgId
* @return true if one of the parts is drm'd audio with rights to save as a ringtone.
*/
public static boolean isDrmRingtoneWithRights(Context context, long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(context, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "isDrmRingtoneWithRights can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
String mimeType = QKSMSApp.getApplication().getDrmManagerClient()
.getOriginalMimeType(part.getDataUri());
if (ContentType.isAudioType(mimeType) && DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.RINGTONE)) {
return true;
}
}
}
return false;
}
/**
* Copies media from an Mms to the "download" directory on the SD card. If any of the parts
* are audio types, drm'd or not, they're copied to the "Ringtones" directory.
*
* @param context
* @param msgId
*/
public static boolean copyMedia(Context context, long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(context, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyMedia can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
// all parts have to be successful for a valid result.
result &= copyPart(context, part, Long.toHexString(msgId));
}
return result;
}
/**
* Returns true if all drm'd parts are forwardable.
*
* @param context
* @param msgId
* @return true if all drm'd parts are forwardable.
*/
public static boolean isForwardable(Context context, long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(context, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "getDrmMimeType can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type) && !DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.TRANSFER)) {
return false;
}
}
return true;
}
public static void addToContacts(Context context, MessageItem msgItem) {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, msgItem.mAddress);
context.startActivity(intent);
}
public static void copyToClipboard(Context context, String str) {
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(null, str));
}
public static void forwardMessage(Context context, MessageItem msgItem) {
Intent forwardIntent = new Intent(context, QKComposeActivity.class);
forwardIntent.putExtra("sms_body", msgItem.mBody);
context.startActivity(forwardIntent);
}
public static void lockMessage(Context context, MessageItem msgItem, boolean locked) {
Uri uri;
if ("sms".equals(msgItem.mType)) {
uri = Sms.CONTENT_URI;
} else {
uri = Mms.CONTENT_URI;
}
final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
final ContentValues values = new ContentValues(1);
values.put("locked", locked ? 1 : 0);
new Thread(() -> {
context.getContentResolver().update(lockUri,
values, null, null);
}, "MainActivity.lockMessage").start();
}
/**
* Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
*
* @param context
* @param msgId
*/
public static boolean haveSomethingToCopyToSDCard(Context context, long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(context, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "haveSomethingToCopyToSDCard can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
boolean result = false;
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
}
if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
ContentType.isAudioType(type) || DrmUtils.isDrmType(type)) {
result = true;
break;
}
}
return result;
}
public static int getDrmMimeMenuStringRsrc(Context context, long msgId) {
if (isDrmRingtoneWithRights(context, msgId)) {
return R.string.save_ringtone;
}
return 0;
}
public static Uri getContactUriForEmail(Context context, String emailAddress) {
Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
new String[]{ContactsContract.CommonDataKinds.Email.CONTACT_ID, ContactsContract.Contacts.DISPLAY_NAME}, null, null, null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String name = cursor.getString(1);
if (!TextUtils.isEmpty(name)) {
return ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, cursor.getLong(0));
}
}
} finally {
cursor.close();
}
}
return null;
}
public static Uri getContactUriForPhoneNumber(String phoneNumber) {
Contact contact = Contact.get(phoneNumber, false);
if (contact.existsInDatabase()) {
return contact.getUri();
}
return null;
}
interface ResizeImageResultCallback {
void onResizeResult(PduPart part, boolean append);
}
private static final String TAG = LogTag.TAG;
private static String sLocalNumber;
private static String[] sNoSubjectStrings;
// Cache of both groups of space-separated ids to their full
// comma-separated display names, as well as individual ids to
// display names.
// TODO: is it possible for canonical address ID keys to be
// re-used? SQLite does reuse IDs on NULL id_ insert, but does
// anything ever delete from the mmssms.db canonical_addresses
// table? Nothing that I could find.
private static final Map<String, String> sRecipientAddress =
new ConcurrentHashMap<>(20 /* initial capacity */);
// When we pass a video record duration to the video recorder, use one of these values.
private static final int[] sVideoDuration =
new int[]{0, 5, 10, 15, 20, 30, 40, 50, 60, 90, 120};
/**
* MMS address parsing data structures
*/
// allowable phone number separators
private static final char[] NUMERIC_CHARS_SUGAR = {
'-', '.', ',', '(', ')', ' ', '/', '\\', '*', '#', '+'
};
private static HashMap numericSugarMap = new HashMap(NUMERIC_CHARS_SUGAR.length);
static {
for (int i = 0; i < NUMERIC_CHARS_SUGAR.length; i++) {
numericSugarMap.put(NUMERIC_CHARS_SUGAR[i], NUMERIC_CHARS_SUGAR[i]);
}
}
private MessageUtils() {
// Forbidden being instantiated.
}
/**
* cleanseMmsSubject will take a subject that's says, "<Subject: no subject>", and return
* a null string. Otherwise it will return the original subject string.
*
* @param context a regular context so the function can grab string resources
* @param subject the raw subject
* @param blacklist any extra strings to cleanse
* @return
*/
public static String cleanseMmsSubject(Context context, String subject, String... blacklist) {
if (TextUtils.isEmpty(subject)) {
return subject;
}
if (sNoSubjectStrings == null) {
sNoSubjectStrings =
context.getResources().getStringArray(R.array.empty_subject_strings);
}
for (String string : sNoSubjectStrings) {
if (subject.replaceAll("\\s+", "")
.equalsIgnoreCase(string.replaceAll("\\s+", ""))) {
return null;
}
}
for (String string : blacklist) {
if (string != null) {
if (subject.replaceAll("\\s+", "")
.equalsIgnoreCase(string.replaceAll("\\s+", ""))) {
return null;
}
}
}
return subject;
}
public static String getMessageDetails(Context context, Cursor cursor, int size) {
if (cursor == null) {
return null;
}
if ("mms".equals(cursor.getString(MessageColumns.COLUMN_MSG_TYPE))) {
int type = cursor.getInt(MessageColumns.COLUMN_MMS_MESSAGE_TYPE);
switch (type) {
case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
return getNotificationIndDetails(context, cursor);
case PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF:
case PduHeaders.MESSAGE_TYPE_SEND_REQ:
return getMultimediaMessageDetails(context, cursor, size);
default:
Log.w(TAG, "No details could be retrieved.");
return "";
}
} else {
return getTextMessageDetails(context, cursor);
}
}
private static String getNotificationIndDetails(Context context, Cursor cursor) {
StringBuilder details = new StringBuilder();
Resources res = context.getResources();
long id = cursor.getLong(MessageColumns.COLUMN_ID);
Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, id);
NotificationInd nInd;
try {
nInd = (NotificationInd) PduPersister.getPduPersister(
context).load(uri);
} catch (MmsException e) {
Log.e(TAG, "Failed to load the message: " + uri, e);
return context.getResources().getString(R.string.cannot_get_details);
}
// Message Type: Mms Notification.
details.append(res.getString(R.string.message_type_label));
details.append(res.getString(R.string.multimedia_notification));
// From: ***
String from = extractEncStr(context, nInd.getFrom());
details.append("\n\n");
details.append(res.getString(R.string.from_label));
details.append(!TextUtils.isEmpty(from) ? from :
res.getString(R.string.hidden_sender_address));
// Date: ***
details.append("\n\n");
details.append(res.getString(
R.string.expire_on,
MessageUtils.formatTimeStampString(
context, nInd.getExpiry() * 1000L, true)));
// Subject: ***
details.append("\n\n");
details.append(res.getString(R.string.subject_label));
EncodedStringValue subject = nInd.getSubject();
if (subject != null) {
details.append(subject.getString());
}
// Message class: Personal/Advertisement/Infomational/Auto
details.append("\n\n");
details.append(res.getString(R.string.message_class_label));
details.append(new String(nInd.getMessageClass()));
// Message size: *** KB
details.append("\n\n");
details.append(res.getString(R.string.message_size_label));
details.append(String.valueOf((nInd.getMessageSize() + 1023) / 1024));
details.append(context.getString(R.string.kilobyte));
return details.toString();
}
private static String getMultimediaMessageDetails(
Context context, Cursor cursor, int size) {
int type = cursor.getInt(MessageColumns.COLUMN_MMS_MESSAGE_TYPE);
if (type == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND) {
return getNotificationIndDetails(context, cursor);
}
StringBuilder details = new StringBuilder();
Resources res = context.getResources();
long id = cursor.getLong(MessageColumns.COLUMN_ID);
Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, id);
MultimediaMessagePdu msg;
try {
msg = (MultimediaMessagePdu) PduPersister.getPduPersister(
context).load(uri);
} catch (MmsException e) {
Log.e(TAG, "Failed to load the message: " + uri, e);
return context.getResources().getString(R.string.cannot_get_details);
}
// Message Type: Text message.
details.append(res.getString(R.string.message_type_label));
details.append(res.getString(R.string.multimedia_message));
if (msg instanceof RetrieveConf) {
// From: ***
String from = extractEncStr(context, ((RetrieveConf) msg).getFrom());
details.append("\n\n");
details.append(res.getString(R.string.from_label));
details.append(!TextUtils.isEmpty(from) ? from :
res.getString(R.string.hidden_sender_address));
}
// To: ***
details.append("\n\n");
details.append(res.getString(R.string.to_address_label));
EncodedStringValue[] to = msg.getTo();
if (to != null) {
details.append(EncodedStringValue.concat(to));
} else {
Log.w(TAG, "recipient list is empty!");
}
// Bcc: ***
if (msg instanceof SendReq) {
EncodedStringValue[] values = ((SendReq) msg).getBcc();
if ((values != null) && (values.length > 0)) {
details.append("\n\n");
details.append(res.getString(R.string.bcc_label));
details.append(EncodedStringValue.concat(values));
}
}
// Date: ***
details.append("\n\n");
int msgBox = cursor.getInt(MessageColumns.COLUMN_MMS_MESSAGE_BOX);
if (msgBox == Mms.MESSAGE_BOX_DRAFTS) {
details.append(res.getString(R.string.saved_label));
} else if (msgBox == Mms.MESSAGE_BOX_INBOX) {
details.append(res.getString(R.string.received_label));
} else {
details.append(res.getString(R.string.sent_label));
}
details.append(MessageUtils.formatTimeStampString(
context, msg.getDate() * 1000L, true));
// Subject: ***
details.append("\n\n");
details.append(res.getString(R.string.subject_label));
EncodedStringValue subject = msg.getSubject();
if (subject != null) {
String subStr = subject.getString();
// Message size should include size of subject.
size += subStr.length();
details.append(subStr);
}
// Priority: High/Normal/Low
details.append("\n\n");
details.append(res.getString(R.string.priority_label));
details.append(getPriorityDescription(context, msg.getPriority()));
// Message size: *** KB
details.append("\n\n");
details.append(res.getString(R.string.message_size_label));
details.append((size - 1) / 1000 + 1);
details.append(" KB");
return details.toString();
}
private static String getTextMessageDetails(Context context, Cursor cursor) {
Log.d(TAG, "getTextMessageDetails");
StringBuilder details = new StringBuilder();
Resources res = context.getResources();
// Message Type: Text message.
details.append(res.getString(R.string.message_type_label));
details.append(res.getString(R.string.text_message));
// Address: ***
details.append("\n\n");
int smsType = cursor.getInt(MessageColumns.COLUMN_SMS_TYPE);
if (SmsHelper.isOutgoingFolder(smsType)) {
details.append(res.getString(R.string.to_address_label));
} else {
details.append(res.getString(R.string.from_label));
}
details.append(cursor.getString(MessageColumns.COLUMN_SMS_ADDRESS));
// Sent: ***
if (smsType == Sms.MESSAGE_TYPE_INBOX) {
long date_sent = cursor.getLong(MessageColumns.COLUMN_SMS_DATE_SENT);
if (date_sent > 0) {
details.append("\n\n");
details.append(res.getString(R.string.sent_label));
details.append(MessageUtils.formatTimeStampString(context, date_sent, true));
}
}
// Received: ***
details.append("\n\n");
if (smsType == Sms.MESSAGE_TYPE_DRAFT) {
details.append(res.getString(R.string.saved_label));
} else if (smsType == Sms.MESSAGE_TYPE_INBOX) {
details.append(res.getString(R.string.received_label));
} else {
details.append(res.getString(R.string.sent_label));
}
long date = cursor.getLong(MessageColumns.COLUMN_SMS_DATE);
details.append(MessageUtils.formatTimeStampString(context, date, true));
// Delivered: ***
if (smsType == Sms.MESSAGE_TYPE_SENT) {
// For sent messages with delivery reports, we stick the delivery time in the
// date_sent column (see MessageStatusReceiver).
long dateDelivered = cursor.getLong(MessageColumns.COLUMN_SMS_DATE_SENT);
if (dateDelivered > 0) {
details.append("\n\n");
details.append(res.getString(R.string.delivered_label));
details.append(MessageUtils.formatTimeStampString(context, dateDelivered, true));
}
}
// Error code: ***
int errorCode = cursor.getInt(MessageColumns.COLUMN_SMS_ERROR_CODE);
if (errorCode != 0) {
details.append("\n\n")
.append(res.getString(R.string.error_code_label))
.append(errorCode);
}
return details.toString();
}
static private String getPriorityDescription(Context context, int PriorityValue) {
Resources res = context.getResources();
switch (PriorityValue) {
case PduHeaders.PRIORITY_HIGH:
return res.getString(R.string.priority_high);
case PduHeaders.PRIORITY_LOW:
return res.getString(R.string.priority_low);
case PduHeaders.PRIORITY_NORMAL:
default:
return res.getString(R.string.priority_normal);
}
}
public static int getAttachmentType(SlideshowModel model, MultimediaMessagePdu mmp) {
if (model == null || mmp == null) {
return MessageItem.ATTACHMENT_TYPE_NOT_LOADED;
}
int numberOfSlides = model.size();
if (numberOfSlides > 1) {
return SmsHelper.SLIDESHOW;
} else if (numberOfSlides == 1) {
// Only one slide in the slide-show.
SlideModel slide = model.get(0);
if (slide.hasVideo()) {
return SmsHelper.VIDEO;
}
if (slide.hasAudio() && slide.hasImage()) {
return SmsHelper.SLIDESHOW;
}
if (slide.hasAudio()) {
return SmsHelper.AUDIO;
}
if (slide.hasImage()) {
return SmsHelper.IMAGE;
}
if (slide.hasText()) {
return SmsHelper.TEXT;
}
// Handle the multimedia message only has subject
String subject = mmp.getSubject() != null ? mmp.getSubject().getString() : null;
if (!TextUtils.isEmpty(subject)) {
return SmsHelper.TEXT;
}
}
return MessageItem.ATTACHMENT_TYPE_NOT_LOADED;
}
public static void removeThumbnailsFromCache(SlideshowModel slideshow) {
if (slideshow != null) {
ThumbnailManager thumbnailManager = QKSMSApp.getApplication().getThumbnailManager();
boolean removedSomething = false;
Iterator<SlideModel> iterator = slideshow.iterator();
while (iterator.hasNext()) {
SlideModel slideModel = iterator.next();
if (slideModel.hasImage()) {
thumbnailManager.removeThumbnail(slideModel.getImage().getUri());
removedSomething = true;
} else if (slideModel.hasVideo()) {
thumbnailManager.removeThumbnail(slideModel.getVideo().getUri());
removedSomething = true;
}
}
if (removedSomething) {
// HACK: the keys to the thumbnail cache are the part uris, such as mms/part/3
// Because the part table doesn't have auto-increment ids, the part ids are reused
// when a message or thread is deleted. For now, we're clearing the whole thumbnail
// cache so we don't retrieve stale images when part ids are reused. This will be
// fixed in the next release in the mms provider.
QKSMSApp.getApplication().getThumbnailManager().clearBackingStore();
}
}
}
public static String formatTimeStampString(Context context, long when) {
return formatTimeStampString(context, when, false);
}
public static String formatTimeStampString(Context context, long when, boolean fullFormat) {
Time then = new Time();
then.set(when);
Time now = new Time();
now.setToNow();
// Basic settings for formatDateTime() we want for all cases.
int format_flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT |
DateUtils.FORMAT_ABBREV_ALL |
DateUtils.FORMAT_CAP_AMPM;
// If the message is from a different year, show the date and year.
if (then.year != now.year) {
format_flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
} else if (then.yearDay != now.yearDay) {
// If it is from a different day than today, show only the date.
format_flags |= DateUtils.FORMAT_SHOW_DATE;
} else {
// Otherwise, if the message is from today, show the time.
format_flags |= DateUtils.FORMAT_SHOW_TIME;
}
// If the caller has asked for full details, make sure to show the date
// and time no matter what we've determined above (but still make showing
// the year only happen if it is a different year from today).
if (fullFormat) {
format_flags |= (DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
}
return DateUtils.formatDateTime(context, when, format_flags);
}
public static void selectAudio(Activity activity, int requestCode) {
Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_INCLUDE_DRM, false);
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE,
activity.getString(R.string.select_audio));
activity.startActivityForResult(intent, requestCode);
}
public static void recordSound(Activity activity, int requestCode, long sizeLimit) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(ContentType.AUDIO_AMR);
intent.setClassName("com.android.soundrecorder",
"com.android.soundrecorder.SoundRecorder");
intent.putExtra(MediaStore.Audio.Media.EXTRA_MAX_BYTES, sizeLimit);
activity.startActivityForResult(intent, requestCode);
}
public static void recordVideo(Activity activity, int requestCode, long sizeLimit) {
// The video recorder can sometimes return a file that's larger than the max we
// say we can handle. Try to handle that overshoot by specifying an 85% limit.
sizeLimit *= .85F;
int durationLimit = getVideoCaptureDurationLimit(sizeLimit);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("recordVideo: durationLimit: " + durationLimit +
" sizeLimit: " + sizeLimit);
}
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
intent.putExtra("android.intent.extra.sizeLimit", sizeLimit);
intent.putExtra("android.intent.extra.durationLimit", durationLimit);
intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
activity.startActivityForResult(intent, requestCode);
}
public static void capturePicture(Activity activity, int requestCode) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
activity.startActivityForResult(intent, requestCode);
}
// Public for until tests
public static int getVideoCaptureDurationLimit(long bytesAvailable) {
CamcorderProfile camcorder = CamcorderProfile.get(CamcorderProfile.QUALITY_LOW);
if (camcorder == null) {
return 0;
}
bytesAvailable *= 8; // convert to bits
long seconds = bytesAvailable / (camcorder.audioBitRate + camcorder.videoBitRate);
// Find the best match for one of the fixed durations
for (int i = sVideoDuration.length - 1; i >= 0; i--) {
if (seconds >= sVideoDuration[i]) {
return sVideoDuration[i];
}
}
return 0;
}
public static void selectVideo(Context context, int requestCode) {
selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED, true);
}
public static void selectImage(Context context, int requestCode) {
selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED, false);
}
private static void selectMediaByType(
Context context, int requestCode, String contentType, boolean localFilesOnly) {
if (context instanceof Activity) {
Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT);
innerIntent.setType(contentType);
if (localFilesOnly) {
innerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
}
Intent wrapperIntent = Intent.createChooser(innerIntent, null);
((Activity) context).startActivityForResult(wrapperIntent, requestCode);
}
}
public static void viewSimpleSlideshow(Context context, SlideshowModel slideshow) {
if (!slideshow.isSimple()) {
throw new IllegalArgumentException(
"viewSimpleSlideshow() called on a non-simple slideshow");
}
SlideModel slide = slideshow.get(0);
MediaModel mm = null;
if (slide.hasImage()) {
mm = slide.getImage();
} else if (slide.hasVideo()) {
mm = slide.getVideo();
}
if (mm == null) {
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra("SingleItemOnly", true); // So we don't see "surrounding" images in Gallery
String contentType;
contentType = mm.getContentType();
intent.setDataAndType(mm.getUri(), contentType);
context.startActivity(intent);
}
public static void showErrorDialog(Activity activity,
String title, String message) {
if (activity.isFinishing()) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setIcon(R.drawable.ic_error);
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
dialog.dismiss();
}
}
});
builder.show();
}
/**
* The quality parameter which is used to compress JPEG images.
*/
public static final int IMAGE_COMPRESSION_QUALITY = 95;
/**
* The minimum quality parameter which is used to compress JPEG images.
*/
public static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;
/**
* Message overhead that reduces the maximum image byte size.
* 5000 is a realistic overhead number that allows for user to also include
* a small MIDI file or a couple pages of text along with the picture.
*/
public static final int MESSAGE_OVERHEAD = 5000;
public static void resizeImageAsync(final Context context,
final Uri imageUri, final Handler handler,
final ResizeImageResultCallback cb,
final boolean append) {
// Show a progress toast if the resize hasn't finished
// within one second.
// Stash the runnable for showing it away so we can cancel
// it later if the resize completes ahead of the deadline.
final Runnable showProgress = new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.compressing, Toast.LENGTH_SHORT).show();
}
};
// Schedule it for one second from now.
handler.postDelayed(showProgress, 1000);
new Thread(new Runnable() {
@Override
public void run() {
final PduPart part;
try {
UriImage image = new UriImage(context, imageUri);
int widthLimit = MmsConfig.getMaxImageWidth();
int heightLimit = MmsConfig.getMaxImageHeight();
// In mms_config.xml, the max width has always been declared larger than the max
// height. Swap the width and height limits if necessary so we scale the picture
// as little as possible.
if (image.getHeight() > image.getWidth()) {
int temp = widthLimit;
widthLimit = heightLimit;
heightLimit = temp;
}
part = image.getResizedImageAsPart(
widthLimit,
heightLimit,
MmsConfig.getMaxMessageSize() - MESSAGE_OVERHEAD);
} finally {
// Cancel pending show of the progress toast if necessary.
handler.removeCallbacks(showProgress);
}
handler.post(new Runnable() {
@Override
public void run() {
cb.onResizeResult(part, append);
}
});
}
}, "MessageUtils.resizeImageAsync").start();
}
public static void showDiscardDraftConfirmDialog(Context context,
OnClickListener listener) {
new AlertDialog.Builder(context)
.setMessage(R.string.discard_message_reason)
.setPositiveButton(R.string.yes, listener)
.setNegativeButton(R.string.cancel, null)
.show();
}
public static String getLocalNumber() {
if (null == sLocalNumber) {
sLocalNumber = QKSMSApp.getApplication().getTelephonyManager().getLine1Number();
}
return sLocalNumber;
}
public static boolean isLocalNumber(String number) {
if (number == null) {
return false;
}
// we don't use Mms.isEmailAddress() because it is too strict for comparing addresses like
// "foo+caf_=6505551212=tmomail.net@gmail.com", which is the 'from' address from a forwarded email
// message from Gmail. We don't want to treat "foo+caf_=6505551212=tmomail.net@gmail.com" and
// "6505551212" to be the same.
return number.indexOf('@') < 0 && PhoneNumberUtils.compare(number, getLocalNumber());
}
public static void handleReadReport(final Context context,
final Collection<Long> threadIds,
final int status,
final Runnable callback) {
StringBuilder selectionBuilder = new StringBuilder(Mms.MESSAGE_TYPE + " = "
+ PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF
+ " AND " + Mms.READ + " = 0"
+ " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES);
String[] selectionArgs = null;
if (threadIds != null) {
String threadIdSelection = null;
StringBuilder buf = new StringBuilder();
selectionArgs = new String[threadIds.size()];
int i = 0;
for (long threadId : threadIds) {
if (i > 0) {
buf.append(" OR ");
}
buf.append(Mms.THREAD_ID).append("=?");
selectionArgs[i++] = Long.toString(threadId);
}
threadIdSelection = buf.toString();
selectionBuilder.append(" AND (" + threadIdSelection + ")");
}
final Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
Mms.Inbox.CONTENT_URI, new String[]{Mms._ID, Mms.MESSAGE_ID},
selectionBuilder.toString(), selectionArgs, null);
if (c == null) {
return;
}
final Map<String, String> map = new HashMap<>();
try {
if (c.getCount() == 0) {
if (callback != null) {
callback.run();
}
return;
}
while (c.moveToNext()) {
Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, c.getLong(0));
map.put(c.getString(1), AddressUtils.getFrom(context, uri));
}
} finally {
c.close();
}
OnClickListener positiveListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
for (final Map.Entry<String, String> entry : map.entrySet()) {
MmsMessageSender.sendReadRec(context, entry.getValue(),
entry.getKey(), status);
}
if (callback != null) {
callback.run();
}
dialog.dismiss();
}
};
OnClickListener negativeListener = new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (callback != null) {
callback.run();
}
dialog.dismiss();
}
};
OnCancelListener cancelListener = new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
if (callback != null) {
callback.run();
}
dialog.dismiss();
}
};
confirmReadReportDialog(context, positiveListener,
negativeListener,
cancelListener);
}
private static void confirmReadReportDialog(Context context,
OnClickListener positiveListener, OnClickListener negativeListener,
OnCancelListener cancelListener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setCancelable(true);
builder.setTitle(R.string.confirm);
builder.setMessage(R.string.message_send_read_report);
builder.setPositiveButton(R.string.yes, positiveListener);
builder.setNegativeButton(R.string.cancel, negativeListener);
builder.setOnCancelListener(cancelListener);
builder.show();
}
public static String extractEncStrFromCursor(Cursor cursor,
int columnRawBytes, int columnCharset) {
String rawBytes = cursor.getString(columnRawBytes);
int charset = cursor.getInt(columnCharset);
if (TextUtils.isEmpty(rawBytes)) {
return "";
} else if (charset == CharacterSets.ANY_CHARSET) {
return rawBytes;
} else {
return new EncodedStringValue(charset, PduPersister.getBytes(rawBytes)).getString();
}
}
private static String extractEncStr(Context context, EncodedStringValue value) {
if (value != null) {
return value.getString();
} else {
return "";
}
}
public static ArrayList<String> extractUris(URLSpan[] spans) {
int size = spans.length;
ArrayList<String> accumulator = new ArrayList<>();
for (int i = 0; i < size; i++) {
accumulator.add(spans[i].getURL());
}
return accumulator;
}
/**
* Play/view the message attachments.
* TOOD: We need to save the draft before launching another activity to view the attachments.
* This is hacky though since we will do saveDraft twice and slow down the UI.
* We should pass the slideshow in intent extra to the view activity instead of
* asking it to read attachments from database.
*
* @param activity
* @param msgUri the MMS message URI in database
* @param slideshow the slideshow to save
* @param persister the PDU persister for updating the database
* @param sendReq the SendReq for updating the database
*/
public static void viewMmsMessageAttachment(Activity activity, Uri msgUri,
SlideshowModel slideshow, AsyncDialog asyncDialog) {
viewMmsMessageAttachment(activity, msgUri, slideshow, 0, asyncDialog);
}
public static void viewMmsMessageAttachment(final Activity activity, final Uri msgUri,
final SlideshowModel slideshow, final int requestCode, AsyncDialog asyncDialog) {
boolean isSimple = (slideshow != null) && slideshow.isSimple();
if (isSimple) {
// In attachment-editor mode, we only ever have one slide.
MessageUtils.viewSimpleSlideshow(activity, slideshow);
} else {
// The user wants to view the slideshow. We have to persist the slideshow parts
// in a background task. If the task takes longer than a half second, a progress dialog
// is displayed. Once the PDU persisting is done, another runnable on the UI thread get
// executed to start the SlideshowActivity.
asyncDialog.runAsync(new Runnable() {
@Override
public void run() {
// If a slideshow was provided, save it to disk first.
if (slideshow != null) {
PduPersister persister = PduPersister.getPduPersister(activity);
try {
PduBody pb = slideshow.toPduBody();
persister.updateParts(msgUri, pb, null);
slideshow.sync(pb);
} catch (MmsException e) {
Log.e(TAG, "Unable to save message for preview");
return;
}
}
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread to launch the slideshow activity.
launchSlideshowActivity(activity, msgUri, requestCode);
}
}, R.string.building_slideshow_title);
}
}
public static void launchSlideshowActivity(Context context, Uri msgUri, int requestCode) {
// Launch the slideshow activity to play/view.
Intent intent = new Intent(context, SlideshowActivity.class);
intent.setData(msgUri);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (requestCode > 0 && context instanceof Activity) {
((Activity) context).startActivityForResult(intent, requestCode);
} else {
context.startActivity(intent);
}
}
/**
* Debugging
*/
public static void writeHprofDataToFile() {
String filename = Environment.getExternalStorageDirectory() + "/mms_oom_hprof_data";
try {
android.os.Debug.dumpHprofData(filename);
Log.i(TAG, "##### written hprof data to " + filename);
} catch (IOException ex) {
Log.e(TAG, "writeHprofDataToFile: caught " + ex);
}
}
// An alias (or commonly called "nickname") is:
// Nickname must begin with a letter.
// Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
public static boolean isAlias(String string) {
if (!MmsConfig.isAliasEnabled()) {
return false;
}
int len = string == null ? 0 : string.length();
if (len < MmsConfig.getAliasMinChars() || len > MmsConfig.getAliasMaxChars()) {
return false;
}
if (!Character.isLetter(string.charAt(0))) { // Nickname begins with a letter
return false;
}
for (int i = 1; i < len; i++) {
char c = string.charAt(i);
if (!(Character.isLetterOrDigit(c) || c == '.')) {
return false;
}
}
return true;
}
/**
* Given a phone number, return the string without syntactic sugar, meaning parens,
* spaces, slashes, dots, dashes, etc. If the input string contains non-numeric
* non-punctuation characters, return null.
*/
private static String parsePhoneNumberForMms(String address) {
StringBuilder builder = new StringBuilder();
int len = address.length();
for (int i = 0; i < len; i++) {
char c = address.charAt(i);
// accept the first '+' in the address
if (c == '+' && builder.length() == 0) {
builder.append(c);
continue;
}
if (Character.isDigit(c)) {
builder.append(c);
continue;
}
if (numericSugarMap.get(c) == null) {
return null;
}
}
return builder.toString();
}
/**
* Returns true if the address passed in is a valid MMS address.
*/
public static boolean isValidMmsAddress(String address) {
String retVal = parseMmsAddress(address);
return (retVal != null);
}
/**
* parse the input address to be a valid MMS address.
* - if the address is an email address, leave it as is.
* - if the address can be parsed into a valid MMS phone number, return the parsed number.
* - if the address is a compliant alias address, leave it as is.
*/
public static String parseMmsAddress(String address) {
// if it's a valid Email address, use that.
if (SmsHelper.isEmailAddress(address)) {
return address;
}
// if we are able to parse the address to a MMS compliant phone number, take that.
String retVal = parsePhoneNumberForMms(address);
if (retVal != null && retVal.length() != 0) {
return retVal;
}
// if it's an alias compliant address, use that.
if (isAlias(address)) {
return address;
}
// it's not a valid MMS address, return null
return null;
}
private static void log(String msg) {
Log.d(TAG, "[MsgUtils] " + msg);
}
}