blob: 502bfde122da52d99f15b7e2d2cab4a5010e1eff [file] [log] [blame]
/*
* 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.android.mms.ui;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentUris;
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.media.CamcorderProfile;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.telephony.PhoneNumberUtils;
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.widget.Toast;
import com.android.mms.LogTag;
import com.android.mms.MmsApp;
import com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.TempFileProvider;
import com.android.mms.data.WorkingMessage;
import com.android.mms.model.MediaModel;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.transaction.MmsMessageSender;
import com.android.mms.util.AddressUtils;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.CharacterSets;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.MultimediaMessagePdu;
import com.google.android.mms.pdu.NotificationInd;
import com.google.android.mms.pdu.PduBody;
import com.google.android.mms.pdu.PduHeaders;
import com.google.android.mms.pdu.PduPart;
import com.google.android.mms.pdu.PduPersister;
import com.google.android.mms.pdu.RetrieveConf;
import com.google.android.mms.pdu.SendReq;
/**
* An utility class for managing messages.
*/
public class MessageUtils {
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<String, String>(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
* @return
*/
public static String cleanseMmsSubject(Context context, String subject) {
if (TextUtils.isEmpty(subject)) {
return subject;
}
if (sNoSubjectStrings == null) {
sNoSubjectStrings =
context.getResources().getStringArray(R.array.empty_subject_strings);
}
final int len = sNoSubjectStrings.length;
for (int i = 0; i < len; i++) {
if (subject.equalsIgnoreCase(sNoSubjectStrings[i])) {
return null;
}
}
return subject;
}
public static String getMessageDetails(Context context, Cursor cursor, int size) {
if (cursor == null) {
return null;
}
if ("mms".equals(cursor.getString(MessageListAdapter.COLUMN_MSG_TYPE))) {
int type = cursor.getInt(MessageListAdapter.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(MessageListAdapter.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');
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');
details.append(res.getString(
R.string.expire_on,
MessageUtils.formatTimeStampString(
context, nInd.getExpiry() * 1000L, true)));
// Subject: ***
details.append('\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');
details.append(res.getString(R.string.message_class_label));
details.append(new String(nInd.getMessageClass()));
// Message size: *** KB
details.append('\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(MessageListAdapter.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(MessageListAdapter.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');
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');
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');
details.append(res.getString(R.string.bcc_label));
details.append(EncodedStringValue.concat(values));
}
}
// Date: ***
details.append('\n');
int msgBox = cursor.getInt(MessageListAdapter.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');
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');
details.append(res.getString(R.string.priority_label));
details.append(getPriorityDescription(context, msg.getPriority()));
// Message size: *** KB
details.append('\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');
int smsType = cursor.getInt(MessageListAdapter.COLUMN_SMS_TYPE);
if (Sms.isOutgoingFolder(smsType)) {
details.append(res.getString(R.string.to_address_label));
} else {
details.append(res.getString(R.string.from_label));
}
details.append(cursor.getString(MessageListAdapter.COLUMN_SMS_ADDRESS));
// Sent: ***
if (smsType == Sms.MESSAGE_TYPE_INBOX) {
long date_sent = cursor.getLong(MessageListAdapter.COLUMN_SMS_DATE_SENT);
if (date_sent > 0) {
details.append('\n');
details.append(res.getString(R.string.sent_label));
details.append(MessageUtils.formatTimeStampString(context, date_sent, true));
}
}
// Received: ***
details.append('\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(MessageListAdapter.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(MessageListAdapter.COLUMN_SMS_DATE_SENT);
if (dateDelivered > 0) {
details.append('\n');
details.append(res.getString(R.string.delivered_label));
details.append(MessageUtils.formatTimeStampString(context, dateDelivered, true));
}
}
// Error code: ***
int errorCode = cursor.getInt(MessageListAdapter.COLUMN_SMS_ERROR_CODE);
if (errorCode != 0) {
details.append('\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) {
if (model == null) {
return MessageItem.ATTACHMENT_TYPE_NOT_LOADED;
}
int numberOfSlides = model.size();
if (numberOfSlides > 1) {
return WorkingMessage.SLIDESHOW;
} else if (numberOfSlides == 1) {
// Only one slide in the slide-show.
SlideModel slide = model.get(0);
if (slide.hasVideo()) {
return WorkingMessage.VIDEO;
}
if (slide.hasAudio() && slide.hasImage()) {
return WorkingMessage.SLIDESHOW;
}
if (slide.hasAudio()) {
return WorkingMessage.AUDIO;
}
if (slide.hasImage()) {
return WorkingMessage.IMAGE;
}
if (slide.hasText()) {
return WorkingMessage.TEXT;
}
}
return MessageItem.ATTACHMENT_TYPE_NOT_LOADED;
}
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(android.provider.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();
}
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_sms_mms_not_delivered);
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.no, null)
.show();
}
public static String getLocalNumber() {
if (null == sLocalNumber) {
sLocalNumber = MmsApp.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.
if (number.indexOf('@') >= 0) {
return false;
}
return 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<String, String>();
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.no, 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<String>();
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) ? false : 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 (Mms.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) {
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);
}
}