/* * 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); } }