package net.everythingandroid.smspopup.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.everythingandroid.smspopup.BuildConfig;
import net.everythingandroid.smspopup.R;
import net.everythingandroid.smspopup.provider.SmsMmsMessage;
import net.everythingandroid.smspopup.provider.SmsPopupContract.ContactNotifications;
import net.everythingandroid.smspopup.receiver.SmsReceiver;
import net.everythingandroid.smspopup.util.ManagePreferences.Defaults;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsMessage;
import android.text.TextUtils;
public class SmsPopupUtils {
// Content URIs for SMS app, these may change in future SDK
public static final Uri MMS_SMS_CONTENT_URI = Uri.parse("content://mms-sms/");
public static final Uri THREAD_ID_CONTENT_URI =
Uri.withAppendedPath(MMS_SMS_CONTENT_URI, "threadID");
public static final Uri CONVERSATION_CONTENT_URI =
Uri.withAppendedPath(MMS_SMS_CONTENT_URI, "conversations");
public static final String SMSTO_URI = "smsto:";
private static final String UNREAD_CONDITION = "read=0";
public static final Uri SMS_CONTENT_URI = Uri.parse("content://sms");
public static final Uri SMS_INBOX_CONTENT_URI = Uri.withAppendedPath(SMS_CONTENT_URI, "inbox");
public static final Uri MMS_CONTENT_URI = Uri.parse("content://mms");
public static final Uri MMS_INBOX_CONTENT_URI = Uri.withAppendedPath(MMS_CONTENT_URI, "inbox");
public static final String SMSMMS_ID = "_id";
public static final String SMS_MIME_TYPE = "vnd.android-dir/mms-sms";
public static final int READ_THREAD = 1;
public static final int MESSAGE_TYPE_SMS = 1;
public static final int MESSAGE_TYPE_MMS = 2;
// The max size of either the width or height of the contact photo
public static final int CONTACT_PHOTO_MAXSIZE = 1024;
// Bitmap cache
// private static final int bitmapCacheSize = 5;
// private static LruCache<Uri, Bitmap> bitmapCache = null;
private static final String[] AUTHOR_CONTACT_INFO =
{ "Adam K <smspopup@everythingandroid.net>" };
private static final String[] AUTHOR_CONTACT_INFO_DONATE =
{ "Adam K <smspopup+donate@everythingandroid.net>" };
public static final Uri DONATE_PAYPAL_URI =
Uri.parse("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8246419");
public static final Uri DONATE_MARKET_URI =
Uri.parse("market://details?id=net.everythingandroid.smspopupdonate");
public static boolean isHoneycomb() {
// Can use static final constants like HONEYCOMB, declared in later versions
// of the OS since they are inlined at compile time. This is guaranteed behavior.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
public static boolean isICS() {
// Can use static final constants like ICS, declared in later versions
// of the OS since they are inlined at compile time. This is guaranteed behavior.
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}
/**
* Looks up a contacts display name by contact id - if not found, the address (phone number)
* will be formatted and returned instead.
*/
public static String getPersonName(Context context, String id, String address) {
// Check for id, if null return the formatting phone number as the name
if (id == null) {
if (address != null) {
return PhoneNumberUtils.formatNumber(address);
} else {
return null;
}
}
Cursor cursor = context.getContentResolver().query(
Uri.withAppendedPath(Contacts.CONTENT_URI, id),
new String[] { Contacts.DISPLAY_NAME },
null, null, null);
if (cursor != null) {
try {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
String name = cursor.getString(0);
if (BuildConfig.DEBUG)
Log.v("Contact Display Name: " + name);
return name;
}
} finally {
cursor.close();
}
}
if (address != null) {
return PhoneNumberUtils.formatNumber(address);
}
return null;
}
/**
* Looks up a contacts display name by contact lookup key - if not found,
* the address (phone number) will be formatted and returned instead.
* @param context Context.
* @param lookupKey Contact lookup key.
* @param address Address (phone number) that will be returned if the contact cannot be
* found. The address will be formatted before it is returned.
* @return Contact name or null if not found.
*/
public static ContactIdentification getPersonNameByLookup(Context context, String lookupKey,
String contactId) {
// Check for id, if null return the formatting phone number as the name
if (lookupKey == null) {
return null;
}
Uri.Builder builder = Contacts.CONTENT_LOOKUP_URI.buildUpon();
builder.appendPath(lookupKey);
if (contactId != null) {
builder.appendPath(contactId);
}
Uri uri = builder.build();
Cursor cursor = context.getContentResolver().query(
uri,
new String[] { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.DISPLAY_NAME },
null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
final String newId = cursor.getString(0);
final String newLookup = cursor.getString(1);
final String contactName = cursor.getString(2);
if (BuildConfig.DEBUG)Log.v("Contact Display Name: " + contactName);
return new ContactIdentification(newId, newLookup, contactName);
}
} finally {
cursor.close();
}
}
return null;
}
/*
* Class to hold contact lookup info (as of Android 2.0+ we need the id and lookup key)
*/
public static class ContactIdentification {
public String contactId = null;
public String contactLookup = null;
public String contactName = null;
public ContactIdentification(String _contactId, String _contactLookup, String _contactName) {
contactId = _contactId;
contactLookup = _contactLookup;
contactName = _contactName;
}
}
/**
* Looks up a contacts id, given their address (phone number in this case). Returns null if not
* found
*/
public static ContactIdentification getPersonIdFromPhoneNumber(
Context context, String address) {
if (address == null) {
return null;
}
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address)),
new String[] { PhoneLookup._ID, PhoneLookup.DISPLAY_NAME, PhoneLookup.LOOKUP_KEY },
null, null, null);
} catch (IllegalArgumentException e) {
Log.e("getPersonIdFromPhoneNumber(): " + e.toString());
return null;
} catch (Exception e) {
Log.e("getPersonIdFromPhoneNumber(): " + e.toString());
return null;
}
if (cursor != null) {
try {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
String contactId = String.valueOf(cursor.getLong(0));
String contactName = cursor.getString(1);
String contactLookup = cursor.getString(2);
if (BuildConfig.DEBUG)
Log.v("Found person: " + contactId + ", " + contactName + ", "
+ contactLookup);
return new ContactIdentification(contactId, contactLookup, contactName);
}
} finally {
cursor.close();
}
}
return null;
}
/**
* Looks up a contacts id, given their email address. Returns null if not found
*/
public static ContactIdentification getPersonIdFromEmail(Context context, String email) {
if (email == null)
return null;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
Uri.withAppendedPath(
Email.CONTENT_LOOKUP_URI,
Uri.encode(extractAddrSpec(email))),
new String[] { Email.CONTACT_ID, Email.DISPLAY_NAME_PRIMARY, Email.LOOKUP_KEY },
null, null, null);
} catch (IllegalArgumentException e) {
Log.e("getPersonIdFromEmail(): " + e.toString());
return null;
} catch (Exception e) {
Log.e("getPersonIdFromEmail(): " + e.toString());
return null;
}
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
String contactId = String.valueOf(cursor.getLong(0));
String contactName = cursor.getString(1);
String contactLookup = cursor.getString(2);
if (BuildConfig.DEBUG)
Log.v("Found person: " + contactId + ", " + contactName + ", "
+ contactLookup);
return new ContactIdentification(contactId, contactLookup, contactName);
}
} finally {
cursor.close();
}
}
return null;
}
/**
*
* Looks up a contact photo by contact id, returns a Bitmap array that represents their photo
* (or null if not found or there was an error.
*
* I do my own scaling and validation of sizes - Android supports any size for contact photos
* and some apps are adding huge photos to contacts. Doing the scaling myself allows me more
* control over how things play out in those cases.
*
* @param context
* the context
* @param id
* contact id
* @param maxThumbSize
* the max size the thumbnail can be
* @return Bitmap of the contacts photo (null if none or an error)
*/
public static Bitmap getPersonPhoto(Context context, final Uri contactUri,
final int thumbSize) {
if (contactUri == null) {
return null;
}
// First let's just check the dimensions of the contact photo
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// The height and width are stored in 'options' but the photo itself is not loaded
loadContactPhoto(context, contactUri, options);
// Raw height and width of contact photo
final int height = options.outHeight;
final int width = options.outWidth;
if (BuildConfig.DEBUG)
Log.v("Contact photo size = " + height + "x" + width);
// If photo is too large or not found get out
if (height > CONTACT_PHOTO_MAXSIZE || width > CONTACT_PHOTO_MAXSIZE ||
width == 0 || height == 0) {
return null;
}
// This time we're going to do it for real
options.inJustDecodeBounds = false;
int newHeight = thumbSize;
int newWidth = thumbSize;
// If we have an abnormal photo size that's larger than thumbsize then sample it down
boolean sampleDown = false;
if (height > thumbSize || width > thumbSize) {
sampleDown = true;
}
// If the dimensions are not the same then calculate new scaled dimenions
if (height < width) {
if (sampleDown) {
options.inSampleSize = Math.round(height / thumbSize);
}
newHeight = Math.round(thumbSize * height / width);
} else {
if (sampleDown) {
options.inSampleSize = Math.round(width / thumbSize);
}
newWidth = Math.round(thumbSize * width / height);
}
// Fetch the real contact photo (sampled down if needed)
Bitmap contactBitmap = null;
try {
contactBitmap = loadContactPhoto(context, contactUri, options);
} catch (OutOfMemoryError e) {
Log.e("Out of memory when loading contact photo");
}
// Not found or error, get out
if (contactBitmap == null)
return null;
// Bitmap scaled to new height and width
return Bitmap.createScaledBitmap(contactBitmap, newWidth, newHeight, true);
}
public static Bitmap getPersonPhoto(Context context, Uri contactUri) {
if (context == null) {
return null;
}
final Resources res = context.getResources();
final int thumbSize = (int) res.getDimension(R.dimen.contact_thumbnail_size);
final int thumbBorder = (int) res.getDimension(R.dimen.contact_thumbnail_border);
return getPersonPhoto(context, contactUri, thumbSize - thumbBorder);
}
/**
* Opens an InputStream for the person's photo and returns the photo as a Bitmap. If the
* person's photo isn't present returns the placeholderImageResource instead.
*
* @param context
* the Context
* @param id
* the id of the person
* @param options
* the decoding options, can be set to null
*/
@SuppressLint("NewApi")
public static Bitmap loadContactPhoto(Context context, Uri contactUri,
BitmapFactory.Options options) {
if (contactUri == null) {
return null;
}
final InputStream stream;
if (SmsPopupUtils.isICS()) {
stream = Contacts.openContactPhotoInputStream(context.getContentResolver(),
contactUri, true);
} else {
stream = Contacts.openContactPhotoInputStream(context.getContentResolver(),
contactUri);
}
return stream != null ? BitmapFactory.decodeStream(stream, null, options) : null;
}
/**
*
* Tries to locate the message thread id given the address (phone or email) of the message
* sender.
*
* @param context
* a context to use
* @param address
* phone number or email address of sender
* @return the thread id (or 0 if there was a problem)
*/
synchronized public static long findThreadIdFromAddress(Context context, String address) {
if (address == null)
return 0;
String THREAD_RECIPIENT_QUERY = "recipient";
Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
uriBuilder.appendQueryParameter(THREAD_RECIPIENT_QUERY, address);
long threadId = 0;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uriBuilder.build(),
new String[] { Contacts._ID },
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
threadId = cursor.getLong(0);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return threadId;
}
/**
* Marks a specific message as read
*/
synchronized public static void setMessageRead(
Context context, long messageId, int messageType) {
SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean markRead = myPrefs.getBoolean(
context.getString(R.string.pref_markread_key),
Defaults.PREFS_MARK_READ);
if (!markRead) {
return;
}
if (messageId > 0) {
ContentValues values = new ContentValues(1);
values.put("read", READ_THREAD);
Uri messageUri;
if (SmsMmsMessage.MESSAGE_TYPE_MMS == messageType) {
// Used to use URI of MMS_CONTENT_URI and it wasn't working, not sure why
// this is diff to SMS
messageUri = Uri.withAppendedPath(MMS_INBOX_CONTENT_URI, String.valueOf(messageId));
} else if (SmsMmsMessage.MESSAGE_TYPE_SMS == messageType) {
messageUri = Uri.withAppendedPath(SMS_CONTENT_URI, String.valueOf(messageId));
} else {
return;
}
// Log.v("messageUri for marking message read: " + messageUri.toString());
ContentResolver cr = context.getContentResolver();
int result;
try {
result = cr.update(messageUri, values, null, null);
} catch (Exception e) {
result = 0;
}
if (BuildConfig.DEBUG)
Log.v(String.format("message id = %s marked as read, result = %s", messageId,
result));
}
}
/**
* Marks a specific message thread as read - all messages in the thread will be marked read
*/
synchronized public static void setThreadRead(Context context, long threadId) {
SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean markRead = myPrefs.getBoolean(
context.getString(R.string.pref_markread_key),
Defaults.PREFS_MARK_READ);
if (!markRead)
return;
if (threadId > 0) {
ContentValues values = new ContentValues(1);
values.put("read", READ_THREAD);
ContentResolver cr = context.getContentResolver();
int result = 0;
try {
result = cr.update(
ContentUris.withAppendedId(CONVERSATION_CONTENT_URI, threadId),
values, null, null);
} catch (Exception e) {
if (BuildConfig.DEBUG)
Log.v("error marking thread read");
}
if (BuildConfig.DEBUG)
Log.v("thread id " + threadId + " marked as read, result = " + result);
}
}
/**
* Tries to locate the message id (from the system database), given the message thread id, the
* timestamp of the message and the type of message (sms/mms)
*/
synchronized public static long findMessageId(Context context, long threadId, long timestamp,
String body, int messageType) {
long id = 0;
String selection = "body = " + DatabaseUtils.sqlEscapeString(body != null ? body : "");
selection += " and " + UNREAD_CONDITION;
final String sortOrder = "date DESC";
final String[] projection = new String[] { "_id", "date", "thread_id", "body" };
if (threadId > 0) {
if (BuildConfig.DEBUG)
Log.v("Trying to find message ID");
if (SmsMmsMessage.MESSAGE_TYPE_MMS == messageType) {
// It seems MMS timestamps are stored in a seconds, whereas SMS timestamps are in
// millis
selection += " and date = " + (timestamp / 1000);
}
Cursor cursor = context.getContentResolver().query(
ContentUris.withAppendedId(CONVERSATION_CONTENT_URI, threadId),
projection,
selection,
null,
sortOrder);
try {
if (cursor != null && cursor.moveToFirst()) {
id = cursor.getLong(0);
if (BuildConfig.DEBUG)
Log.v("Message id found = " + id);
// Log.v("Timestamp = " + cursor.getLong(1));
}
} finally {
cursor.close();
}
}
if (BuildConfig.DEBUG && id == 0) {
Log.v("Message id could not be found");
}
return id;
}
/**
* Tries to delete a message from the system database, given the thread id, the timestamp of the
* message and the message type (sms/mms).
*/
public static void deleteMessage(Context context, long messageId,
long threadId, int messageType) {
if (messageId > 0) {
if (BuildConfig.DEBUG)
Log.v("id of message to delete is " + messageId);
// We need to mark this message read first to ensure the entire thread is marked as read
setMessageRead(context, messageId, messageType);
// Construct delete message uri
Uri deleteUri;
if (SmsMmsMessage.MESSAGE_TYPE_MMS == messageType) {
deleteUri = Uri.withAppendedPath(MMS_CONTENT_URI, String.valueOf(messageId));
} else if (SmsMmsMessage.MESSAGE_TYPE_SMS == messageType) {
deleteUri = Uri.withAppendedPath(SMS_CONTENT_URI, String.valueOf(messageId));
} else {
return;
}
int count = 0;
try {
count = context.getContentResolver().delete(deleteUri, null, null);
} catch (Exception e) {
if (BuildConfig.DEBUG)
Log.v("deleteMessage(): Problem deleting message - " + e.toString());
}
if (BuildConfig.DEBUG)
Log.v("Messages deleted: " + count);
if (count == 1) {
// TODO: should only set the thread read if there are no more unread messages
// setThreadRead(context, threadId);
}
}
}
/**
* Fetches a list of unread messages from the system database
*
* @param context
* app context
* @param ignoreMessageId
* message id to ignore (the one being displayed), setting this to 0 will return all
* unread messages
*
* @return ArrayList of SmsMmsMessage
*/
public static ArrayList<SmsMmsMessage> getUnreadMessages(Context context) {
ArrayList<SmsMmsMessage> messages = null;
final String[] projection =
new String[] { "_id", "thread_id", "address", "date", "body" };
String selection = UNREAD_CONDITION;
String[] selectionArgs = null;
final String sortOrder = "date ASC";
// Create cursor
Cursor cursor = context.getContentResolver().query(
SMS_INBOX_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder);
long messageId;
long threadId;
String address;
long timestamp;
String body;
SmsMmsMessage message;
if (cursor != null) {
try {
int count = cursor.getCount();
if (count > 0) {
messages = new ArrayList<SmsMmsMessage>(count);
while (cursor.moveToNext()) {
messageId = cursor.getLong(0);
threadId = cursor.getLong(1);
address = cursor.getString(2);
timestamp = cursor.getLong(3);
body = cursor.getString(4);
message = new SmsMmsMessage(
context, address, body, timestamp, threadId,
count, messageId, SmsMmsMessage.MESSAGE_TYPE_SMS);
message.setNotify(false);
messages.add(message);
}
}
} finally {
cursor.close();
}
}
return messages;
}
/**
*
*/
public static Intent getSmsInboxIntent() {
Intent conversations = new Intent(Intent.ACTION_MAIN);
// conversations.addCategory(Intent.CATEGORY_DEFAULT);
conversations.setType(SMS_MIME_TYPE);
// should I be using FLAG_ACTIVITY_RESET_TASK_IF_NEEDED??
int flags =
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP |
Intent.FLAG_ACTIVITY_CLEAR_TOP;
conversations.setFlags(flags);
return conversations;
}
/**
* Get system view sms thread Intent
*
* @param context
* context
* @param threadId
* the message thread id to view
* @return the intent that can be started with startActivity()
*/
public static Intent getSmsToIntent(Context context, long threadId) {
Intent popup = new Intent(Intent.ACTION_VIEW);
// Should *NOT* be using FLAG_ACTIVITY_MULTIPLE_TASK however something is broken on
// a few popular devices that received recent Froyo upgrades that means this is required
// in order to refresh the system compose message UI
int flags =
Intent.FLAG_ACTIVITY_NEW_TASK |
// Intent.FLAG_ACTIVITY_SINGLE_TOP |
Intent.FLAG_ACTIVITY_CLEAR_TOP;
// Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
popup.setFlags(flags);
if (threadId > 0) {
// Log.v("^^Found threadId (" + threadId + "), sending to Sms intent");
popup.setData(Uri.withAppendedPath(THREAD_ID_CONTENT_URI, String.valueOf(threadId)));
} else {
return getSmsInboxIntent();
}
return popup;
}
/**
* Get system sms-to Intent (normally "compose message" activity)
*
* @param context
* context
* @param phoneNumber
* the phone number to compose the message to
* @return the intent that can be started with startActivity()
*/
public static Intent getSmsToIntent(Context context, String phoneNumber) {
Intent popup = new Intent(Intent.ACTION_SENDTO);
// Should *NOT* be using FLAG_ACTIVITY_MULTIPLE_TASK however something is broken on
// a few popular devices that received recent Froyo upgrades that means this is required
// in order to refresh the system compose message UI
int flags =
Intent.FLAG_ACTIVITY_NEW_TASK |
// Intent.FLAG_ACTIVITY_SINGLE_TOP |
Intent.FLAG_ACTIVITY_CLEAR_TOP;
// Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
popup.setFlags(flags);
if (!"".equals(phoneNumber)) {
// Log.v("^^Found threadId (" + threadId + "), sending to Sms intent");
popup.setData(Uri.parse(SMSTO_URI + Uri.encode(phoneNumber)));
} else {
return getSmsInboxIntent();
}
return popup;
}
/**
*
*/
public static void launchEmailToIntent(Context context, String subject, boolean includeDebug) {
Intent msg = new Intent(Intent.ACTION_SEND);
SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean donated = myPrefs.getBoolean(context.getString(R.string.pref_donated_key), false);
StringBuilder body = new StringBuilder();
if (includeDebug) {
body.append(String.format("\n\n----------\nSysinfo - %s\nModel: %s\n\n",
Build.FINGERPRINT, Build.MODEL));
// body.append(String.format("\n\nBrand: %s\n\n", Build.BRAND));
// Array of preference keys to include in email
final String[] pref_keys = {
context.getString(R.string.pref_enabled_key),
context.getString(R.string.pref_timeout_key),
context.getString(R.string.pref_privacy_key),
context.getString(R.string.pref_privacy_sender_key),
context.getString(R.string.pref_privacy_always_key),
context.getString(R.string.pref_dimscreen_key),
context.getString(R.string.pref_markread_key),
context.getString(R.string.pref_onlyShowOnKeyguard_key),
context.getString(R.string.pref_show_buttons_key),
context.getString(R.string.pref_button1_key),
context.getString(R.string.pref_button2_key),
context.getString(R.string.pref_button3_key),
// context.getString(R.string.pref_blur_key),
context.getString(R.string.pref_popup_enabled_key),
context.getString(R.string.pref_notif_enabled_key),
context.getString(R.string.pref_notif_sound_key),
context.getString(R.string.pref_notif_icon_key),
context.getString(R.string.pref_vibrate_key),
context.getString(R.string.pref_vibrate_pattern_key),
context.getString(R.string.pref_vibrate_pattern_custom_key),
context.getString(R.string.pref_flashled_key),
context.getString(R.string.pref_flashled_color_key),
context.getString(R.string.pref_notif_repeat_key),
context.getString(R.string.pref_notif_repeat_times_key),
context.getString(R.string.pref_notif_repeat_interval_key),
};
Map<String, ?> m = myPrefs.getAll();
body.append(String.format("%s config -\n", subject));
for (int i = 0; i < pref_keys.length; i++) {
try {
body.append(String.format("%s: %s\n", pref_keys[i], m.get(pref_keys[i])));
} catch (NullPointerException e) {
// Nothing to do here
}
}
Cursor c = context.getContentResolver().query(
ContactNotifications.CONTENT_URI, null, null, null, null);
int dbRowCount = 0;
if (c != null) {
dbRowCount = c.getCount();
}
body.append("Db Rows: " + dbRowCount + "\n");
// Add locale info
body.append(String.format("locale: %s\n",
context.getResources().getConfiguration().locale.getDisplayName()));
// TODO: fix this up so users can attach system logs to the email
// this almost works but for some reason the attachment never sends (while it still
// appears in the draft email that is created) :(
// Attach the log file if it exists
// Uri log = collectLogs(context);
// if (log != null) {
// msg.putExtra(Intent.EXTRA_STREAM, log);
// }
}
msg.putExtra(Intent.EXTRA_EMAIL, donated ? AUTHOR_CONTACT_INFO_DONATE : AUTHOR_CONTACT_INFO);
msg.putExtra(Intent.EXTRA_SUBJECT, subject);
msg.putExtra(Intent.EXTRA_TEXT, body.toString());
msg.setType("message/rfc822");
context.startActivity(Intent.createChooser(
msg, context.getString(R.string.pref_sendemail_title)));
}
/**
* Fetch output from logcat, dump it in a file and return the URI to the file
*/
public static Uri collectLogs(Context context) {
final String logfile = "log.txt";
try {
ArrayList<String> commandLine = new ArrayList<String>();
commandLine.add("logcat");
commandLine.add("-d");
commandLine.add("AndroidRuntime:E");
commandLine.add(Log.LOGTAG + ":V");
commandLine.add("*:S");
BufferedInputStream fin = new BufferedInputStream(
Runtime.getRuntime().exec(commandLine.toArray(new String[0])).getInputStream());
BufferedOutputStream fout = new BufferedOutputStream(
context.openFileOutput(logfile, Context.MODE_WORLD_READABLE));
// Copy output to a log file
int i;
do {
i = fin.read();
if (i != -1)
fout.write(i);
} while (i != -1);
fin.close();
fout.close();
} catch (IOException e) {
return null;
} catch (SecurityException e) {
return null;
}
return Uri.fromFile(context.getFileStreamPath(logfile));
}
/**
* Return current unread message count from system db (sms and mms)
*
* @param context
* @return unread sms+mms message count
*/
public static int getUnreadMessagesCount(Context context) {
return getUnreadMessagesCount(context, 0, null);
}
/**
* Return current unread message count from system db (sms and mms)
*
* @param context
* @param timestamp
* only messages before this timestamp will be counted
* @return unread sms+mms message count
*/
synchronized public static int getUnreadMessagesCount(Context context, long timestamp,
String messageBody) {
return getUnreadSmsCount(context, timestamp, messageBody) + getUnreadMmsCount(context);
}
/**
* Return current unread message count from system db (sms only)
*
* @param context
* @return unread sms message count
*/
public static int getUnreadSmsCount(Context context) {
return getUnreadSmsCount(context, 0, null);
}
/**
* Return current unread message count from system db (sms only)
*
* @param context
* @param timestamp
* only messages before this timestamp will be counted
* @return unread sms message count
*/
private static int getUnreadSmsCount(Context context, long timestamp, String messageBody) {
if (BuildConfig.DEBUG)
Log.v("getUnreadSmsCount()");
final String[] projection = new String[] { SMSMMS_ID, "body" };
final String selection = UNREAD_CONDITION;
final String[] selectionArgs = null;
final String sortOrder = "date DESC";
int count = 0;
Cursor cursor = context.getContentResolver().query(
SMS_INBOX_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder);
if (cursor != null) {
try {
count = cursor.getCount();
/*
* We need to check if the message received matches the most recent one in the db or
* not (to find out if our code ran before the system code or vice-versa)
*/
if (messageBody != null && count > 0) {
if (cursor.moveToFirst()) {
/*
* Check the most recent message, if the body does not match then it hasn't
* yet been inserted into the system database, therefore we need to add one
* to our total count
*/
if (!messageBody.equals(cursor.getString(1))) {
if (BuildConfig.DEBUG)
Log.v("getUnreadSmsCount(): most recent message did not match body, adding 1 to count");
count++;
}
}
}
} finally {
cursor.close();
}
}
/*
* If count is still 0 and timestamp is set then its likely the system db had not updated
* when this code ran, therefore let's add 1 so the notify will run correctly.
*/
if (count == 0 && timestamp > 0) {
count = 1;
}
if (BuildConfig.DEBUG)
Log.v("getUnreadSmsCount(): unread count = " + count);
return count;
}
/**
* Return current unread message count from system db (mms only)
*
* @param context
* @return unread mms message count
*/
private static int getUnreadMmsCount(Context context) {
final String selection = UNREAD_CONDITION;
final String[] projection = new String[] { SMSMMS_ID };
int count = 0;
Cursor cursor = context.getContentResolver().query(
MMS_INBOX_CONTENT_URI,
projection,
selection, null, null);
if (cursor != null) {
try {
count = cursor.getCount();
} finally {
cursor.close();
}
}
if (BuildConfig.DEBUG)
Log.v("mms unread count = " + count);
return count;
}
/*
*
*/
synchronized public static SmsMmsMessage getSmsDetails(Context context,
long ignoreThreadId, boolean unreadOnly) {
final String[] projection =
new String[] { "_id", "thread_id", "address", "date", "body" };
String selection = unreadOnly ? UNREAD_CONDITION : null;
String[] selectionArgs = null;
final String sortOrder = "date DESC";
int count = 0;
if (ignoreThreadId > 0) {
selection = (selection == null) ? "" : selection + " and ";
selection += "thread_id != ?";
selectionArgs = new String[] { String.valueOf(ignoreThreadId) };
}
Cursor cursor = context.getContentResolver().query(
SMS_INBOX_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder);
if (cursor != null) {
try {
count = cursor.getCount();
if (count > 0) {
cursor.moveToFirst();
long messageId = cursor.getLong(0);
long threadId = cursor.getLong(1);
String address = cursor.getString(2);
long timestamp = cursor.getLong(3);
String body = cursor.getString(4);
if (!unreadOnly) {
count = 0;
}
SmsMmsMessage smsMessage = new SmsMmsMessage(
context, address, body, timestamp, threadId,
count, messageId, SmsMmsMessage.MESSAGE_TYPE_SMS);
return smsMessage;
}
} finally {
cursor.close();
}
}
return null;
}
public static SmsMmsMessage getSmsDetails(Context context) {
return getSmsDetails(context, 0);
}
public static SmsMmsMessage getSmsDetails(Context context, boolean unreadOnly) {
return getSmsDetails(context, 0, unreadOnly);
}
public static SmsMmsMessage getSmsDetails(Context context, long ignoreThreadId) {
return getSmsDetails(context, ignoreThreadId, true);
}
/*
*
*/
synchronized public static SmsMmsMessage getMmsDetails(Context context, long ignoreThreadId) {
final String[] projection = new String[] { "_id", "thread_id", "date", "sub", "sub_cs" };
String selection = UNREAD_CONDITION;
String[] selectionArgs = null;
final String sortOrder = "date DESC";
int count = 0;
if (ignoreThreadId > 0) {
selection += " and thread_id != ?";
selectionArgs = new String[] { String.valueOf(ignoreThreadId) };
}
Cursor cursor = context.getContentResolver().query(
MMS_INBOX_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder);
if (cursor != null) {
try {
count = cursor.getCount();
if (count > 0) {
cursor.moveToFirst();
long messageId = cursor.getLong(0);
long threadId = cursor.getLong(1);
long timestamp = cursor.getLong(2) * 1000;
String subject = cursor.getString(3);
return new SmsMmsMessage(context, messageId, threadId, timestamp,
subject, count, SmsMmsMessage.MESSAGE_TYPE_MMS);
}
} finally {
cursor.close();
}
}
return null;
}
public static SmsMmsMessage getMmsDetails(Context context) {
return getMmsDetails(context, 0);
}
public static String getMmsAddress(Context context, long messageId) {
final String[] projection = new String[] { "address", "contact_id", "charset", "type" };
// final String selection = "type=137"; // "type="+ PduHeaders.FROM,
final String selection = null;
Uri.Builder builder = MMS_CONTENT_URI.buildUpon();
builder.appendPath(String.valueOf(messageId)).appendPath("addr");
Cursor cursor = context.getContentResolver().query(
builder.build(),
projection,
selection,
null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Apparently contact_id is always empty in this table so we can't get it from
// here
// Just return the address
return cursor.getString(0);
}
} finally {
cursor.close();
}
}
return context.getString(android.R.string.unknownName);
}
public static final Pattern NAME_ADDR_EMAIL_PATTERN =
Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
public static final Pattern QUOTED_STRING_PATTERN =
Pattern.compile("\\s*\"([^\"]*)\"\\s*");
private static String extractAddrSpec(String address) {
Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
if (match.matches()) {
return match.group(2);
}
return address;
}
private static String getEmailDisplayName(String displayString) {
Matcher match = QUOTED_STRING_PATTERN.matcher(displayString);
if (match.matches()) {
return match.group(1);
}
return displayString;
}
/**
* Get the display name of an email address. If the address already contains the name, parse and
* return it. Otherwise, query the contact database. Cache query results for repeated queries.
*/
public static String getDisplayName(Context context, String email) {
Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(email);
if (match.matches()) {
// email has display name, return that
return getEmailDisplayName(match.group(1));
}
// otherwise let's check the contacts list for a user with this email
// Cursor cursor = context.getContentResolver().query(
// ContactWrapper.getEmailContentUri(),
// new String[] { Contacts.ContactMethods.NAME },
// Contacts.ContactMethods.DATA + " = ?",
// new String[] { email }, null);
Cursor cursor = context.getContentResolver().query(
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(email)),
new String[] { Contacts.DISPLAY_NAME },
null, null, null);
if (cursor != null) {
try {
// int columnIndex =
// cursor.getColumnIndexOrThrow(Contacts.ContactMethods.NAME);
while (cursor.moveToNext()) {
// String name = cursor.getString(columnIndex);
String name = cursor.getString(0);
if (!TextUtils.isEmpty(name)) {
return name;
}
}
} finally {
cursor.close();
}
}
return email;
}
/*
* Get the most recent unread message, returning in a SmsMmsMessage which is suitable for
* updating the notification. Optional param is the message object: we can pull out the thread
* id of this message in the case the user is "replying" to the message and we should ignore all
* messages in the thread when working out what to display in the notification bar (as these
* messages will soon be marked read but we can't be sure when the messaging app will actually
* start).
*/
public static SmsMmsMessage getRecentMessage(Context context, SmsMmsMessage ignoreMessage) {
long ignoreThreadId = 0;
if (ignoreMessage != null) {
ignoreThreadId = ignoreMessage.getThreadId();
}
SmsMmsMessage smsMessage = getSmsDetails(context, ignoreThreadId);
SmsMmsMessage mmsMessage = getMmsDetails(context, ignoreThreadId);
if (mmsMessage == null && smsMessage != null) {
return smsMessage;
}
if (mmsMessage != null && smsMessage == null) {
return mmsMessage;
}
if (mmsMessage != null && smsMessage != null) {
if (mmsMessage.getTimestamp() < smsMessage.getTimestamp()) {
return mmsMessage;
}
return smsMessage;
}
return null;
}
public static SmsMmsMessage getRecentMessage(Context context) {
return getRecentMessage(context, null);
}
/**
* Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a {@link #DATA_SMS_RECEIVED_ACTION}
* intent.
*
* @param intent
* the intent to read from
* @return an array of SmsMessages for the PDUs
*/
public static final SmsMessage[] getMessagesFromIntent(Intent intent) {
Object[] messages = (Object[]) intent.getSerializableExtra("pdus");
if (messages == null) {
return null;
}
if (messages.length == 0) {
return null;
}
byte[][] pduObjs = new byte[messages.length][];
for (int i = 0; i < messages.length; i++) {
pduObjs[i] = (byte[]) messages[i];
}
byte[][] pdus = new byte[pduObjs.length][];
int pduCount = pdus.length;
SmsMessage[] msgs = new SmsMessage[pduCount];
for (int i = 0; i < pduCount; i++) {
pdus[i] = pduObjs[i];
msgs[i] = SmsMessage.createFromPdu(pdus[i]);
}
return msgs;
}
/**
* This function will see if the most recent activity was the system messaging app so we can
* suppress the popup as the user is likely already viewing messages or composing a new message
*/
public static final boolean inMessagingApp(Context context) {
/*
* These appear to be the 2 main intents that mean the user is using the messaging app
*
* action "android.intent.action.MAIN" data null class "com.android.mms.ui.ConversationList"
* package "com.android.mms"
*
* action "android.intent.action.VIEW" data "content://mms-sms/threadID/3" class
* "com.android.mms.ui.ComposeMessageActivity" package "com.android.mms"
*/
ActivityManager mAM = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> mRunningTaskList = mAM.getRunningTasks(1);
Iterator<RunningTaskInfo> mIterator = mRunningTaskList.iterator();
if (mIterator.hasNext()) {
RunningTaskInfo mRunningTask = mIterator.next();
if (mRunningTask != null) {
ComponentName runningTaskComponent = mRunningTask.baseActivity;
// Log.v("baseActivity = " + mRunningTask.baseActivity.toString());
// Log.v("topActivity = " + mRunningTask.topActivity.toString());
if (SmsMessageSender.MESSAGING_PACKAGE_NAME.equals(runningTaskComponent
.getPackageName())
&&
(SmsMessageSender.MESSAGING_CONVO_CLASS_NAME.equals(runningTaskComponent
.getClassName()))
||
SmsMessageSender.MESSAGING_COMPOSE_CLASS_NAME.equals(runningTaskComponent
.getClassName())) {
if (BuildConfig.DEBUG)
Log.v("User in messaging app - from running task");
return true;
}
}
}
return false;
}
/**
* Enables or disables the main SMS receiver
*/
public static void enableSmsPopup(Context context, boolean enable) {
PackageManager pm = context.getPackageManager();
ComponentName cn = new ComponentName(context, SmsReceiver.class);
// Update preference so it reflects in the preference activity
SharedPreferences myPrefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor settings = myPrefs.edit();
settings.putBoolean(context.getString(R.string.pref_enabled_key), enable);
settings.commit();
if (enable) {
if (BuildConfig.DEBUG)
Log.v("SMSPopup receiver is enabled");
pm.setComponentEnabledSetting(cn,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
PackageManager.DONT_KILL_APP);
} else {
if (BuildConfig.DEBUG)
Log.v("SMSPopup receiver is disabled");
pm.setComponentEnabledSetting(cn,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
}
/**
* Convert from pixels to density independent pixels.
*
* @param res
* Resources to fetch display metrics from.
* @param pixels
* Pixel dimension to convert.
* @return Density independent pixels.
*/
public static int pixelsToDip(Resources res, int pixels) {
final float scale = res.getDisplayMetrics().density;
return (int) (pixels * scale + 0.5f);
}
}