blob: cb963c6e1d183efc7011dc2cc85dda58f81cba43 [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 com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.model.CarrierContentRestriction;
import com.android.mms.transaction.MessagingNotification;
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.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;
import com.google.android.mms.util.SqliteWrapper;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Handler;
import android.provider.Settings;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Threads;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.text.style.URLSpan;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* An utility class for managing messages.
*/
public class MessageUtils {
interface ResizeImageResultCallback {
void onResizeResult(PduPart part);
}
private static final String TAG = "MessageUtils";
private static String sLocalNumber;
// 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 */);
public static final int READ_THREAD = 1;
private MessageUtils() {
// Forbidden being instantiated.
}
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) {
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));
// Date: ***
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));
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 AttachmentEditor.EMPTY;
}
int numberOfSlides = model.size();
if (numberOfSlides > 1) {
return AttachmentEditor.SLIDESHOW_ATTACHMENT;
} else if (numberOfSlides == 1) {
// Only one slide in the slide-show.
SlideModel slide = model.get(0);
if (slide.hasVideo()) {
return AttachmentEditor.VIDEO_ATTACHMENT;
}
if (slide.hasAudio() && slide.hasImage()) {
return AttachmentEditor.SLIDESHOW_ATTACHMENT;
}
if (slide.hasAudio()) {
return AttachmentEditor.AUDIO_ATTACHMENT;
}
if (slide.hasImage()) {
return AttachmentEditor.IMAGE_ATTACHMENT;
}
if (slide.hasText()) {
return AttachmentEditor.TEXT_ONLY;
}
}
return AttachmentEditor.EMPTY;
}
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;
}
return DateUtils.formatDateTime(context, when, format_flags);
}
/**
* @parameter recipientIds space-separated list of ids
*/
public static String getRecipientsByIds(Context context, String recipientIds) {
String value = sRecipientAddress.get(recipientIds);
if (value != null) {
return value;
}
if (!TextUtils.isEmpty(recipientIds)) {
StringBuilder addressBuf = extractIdsToAddresses(context, recipientIds);
if (addressBuf == null) {
// temporary error? Don't memoize.
return "";
}
value = addressBuf.toString();
} else {
value = "";
}
sRecipientAddress.put(recipientIds, value);
return value;
}
private static StringBuilder extractIdsToAddresses(Context context,
String recipients) {
StringBuilder addressBuf = new StringBuilder();
String[] recipientIds = recipients.split(" ");
boolean firstItem = true;
for (String recipientId : recipientIds) {
String value = sRecipientAddress.get(recipientId);
if (value == null) {
Uri uri = Uri.parse("content://mms-sms/canonical-address/" +
recipientId);
Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
uri, null, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
value = c.getString(0);
}
} finally {
c.close();
}
}
}
if (value == null) {
continue;
}
sRecipientAddress.put(recipientId, value);
if (firstItem) {
firstItem = false;
} else {
addressBuf.append(";");
}
addressBuf.append(value);
}
return (addressBuf.length() == 0) ? null : addressBuf;
}
public static String getAddressByThreadId(Context context, long threadId) {
String[] projection = new String[] { Threads.RECIPIENT_IDS };
Uri.Builder builder = Threads.CONTENT_URI.buildUpon();
builder.appendQueryParameter("simple", "true");
Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
builder.build(), projection,
Threads._ID + "=" + threadId, null, null);
if (cursor != null) {
try {
if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
String address = getRecipientsByIds(context,
cursor.getString(0));
if (!TextUtils.isEmpty(address)) {
return address;
}
}
} finally {
cursor.close();
}
}
return null;
}
public static void selectAudio(Context context, int requestCode) {
if (context instanceof Activity) {
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_TITLE,
context.getString(R.string.select_audio));
((Activity) context).startActivityForResult(intent, requestCode);
}
}
public static void recordSound(Context context, int requestCode) {
if (context instanceof Activity) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(ContentType.AUDIO_AMR);
intent.setClassName("com.android.soundrecorder",
"com.android.soundrecorder.SoundRecorder");
((Activity) context).startActivityForResult(intent, requestCode);
}
}
public static void selectVideo(Context context, int requestCode) {
selectMediaByType(context, requestCode, ContentType.VIDEO_UNSPECIFIED);
}
public static void selectImage(Context context, int requestCode) {
selectMediaByType(context, requestCode, ContentType.IMAGE_UNSPECIFIED);
}
private static void selectMediaByType(
Context context, int requestCode, String contentType) {
if (context instanceof Activity) {
Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT);
innerIntent.setType(contentType);
Intent wrapperIntent = Intent.createChooser(innerIntent, null);
((Activity) context).startActivityForResult(wrapperIntent, requestCode);
}
}
public static void showErrorDialog(Context context,
String title, String message) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
builder.setTitle(title);
builder.setMessage(message);
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
}
public static Uri saveBitmapAsPart(Context context, Uri messageUri, Bitmap bitmap)
throws MmsException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
bitmap.compress(CompressFormat.JPEG,
MmsConfig.IMAGE_COMPRESSION_QUALITY, os);
PduPart part = new PduPart();
part.setContentType("image/jpeg".getBytes());
String contentId = "Image" + System.currentTimeMillis();
part.setContentLocation((contentId + ".jpg").getBytes());
part.setContentId(contentId.getBytes());
part.setData(os.toByteArray());
return PduPersister.getPduPersister(context).persistPart(part,
ContentUris.parseId(messageUri));
}
public static void markAsRead(Context context, long threadId) {
ContentValues values = new ContentValues(1);
values.put("read", READ_THREAD);
SqliteWrapper.update(context, context.getContentResolver(),
ContentUris.withAppendedId(Threads.CONTENT_URI, threadId),
values, "read=0", null);
MessagingNotification.updateNewMessageIndicator(context, threadId);
}
public static void resizeImageAsync(final Context context,
final Uri imageUri, final Handler handler,
final ResizeImageResultCallback cb) {
// Show a progress dialog if the resize hasn't finished
// within one second.
// Make the progress dialog.
final ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setTitle(context.getText(R.string.image_too_large));
progressDialog.setMessage(context.getText(R.string.compressing));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
// 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() {
public void run() {
progressDialog.show();
}
};
// Schedule it for one second from now.
handler.postDelayed(showProgress, 1000);
new Thread(new Runnable() {
public void run() {
final PduPart part;
try {
UriImage image = new UriImage(context, imageUri);
part = image.getResizedImageAsPart(
CarrierContentRestriction.IMAGE_WIDTH_LIMIT,
CarrierContentRestriction.IMAGE_HEIGHT_LIMIT);
} finally {
// Cancel pending show of the progress dialog if necessary.
handler.removeCallbacks(showProgress);
// Dismiss the progress dialog if it's around.
progressDialog.dismiss();
}
handler.post(new Runnable() {
public void run() {
cb.onResizeResult(part);
}
});
}
}).start();
}
public static void showDiscardDraftConfirmDialog(Context context,
OnClickListener listener) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.discard_message)
.setMessage(R.string.discard_message_reason)
.setPositiveButton(R.string.yes, listener)
.setNegativeButton(R.string.no, null)
.show();
}
private static String getLocalNumber() {
if (null == sLocalNumber) {
sLocalNumber = TelephonyManager.getDefault().getLine1Number();
}
return sLocalNumber;
}
public static boolean isLocalNumber(String number) {
return PhoneNumberUtils.compare(number, getLocalNumber());
}
public static void handleReadReport(final Context context,
final long threadId,
final int status,
final Runnable callback) {
String selection = Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF
+ " AND " + Mms.READ + " = 0"
+ " AND " + Mms.READ_REPORT + " = " + PduHeaders.VALUE_YES;
if (threadId != -1) {
selection = selection + " AND " + Mms.THREAD_ID + " = " + threadId;
}
final Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
Mms.Inbox.CONTENT_URI, new String[] {Mms._ID, Mms.MESSAGE_ID},
selection, null, 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() {
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();
}
}
};
OnClickListener negativeListener = new OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (callback != null) {
callback.run();
}
}
};
OnCancelListener cancelListener = new OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (callback != null) {
callback.run();
}
}
};
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;
}
}