Java tutorial
/* * Copyright (C) 2010 Felix Bechstein * * This file is part of SMSdroid. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 3 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; If not, see <http://www.gnu.org/licenses/>. */ package de.ub0r.android.smsdroid; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.CallLog.Calls; import android.provider.Telephony; import android.support.v4.app.NotificationCompat; import android.telephony.SmsMessage; import android.text.TextUtils; import android.util.TypedValue; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.ub0r.android.logg0r.Log; /** * Listen for new sms. * * @author flx */ @SuppressWarnings("deprecation") public class SmsReceiver extends BroadcastReceiver { /** * Tag for logging. */ static final String TAG = "bcr"; /** * {@link Uri} to get messages from. */ private static final Uri URI_SMS = Uri.parse("content://sms/"); /** * {@link Uri} to get messages from. */ private static final Uri URI_MMS = Uri.parse("content://mms/"); /** * Intent.action for receiving SMS. */ @SuppressLint("InlinedApi") private static final String ACTION_SMS_OLD = Telephony.Sms.Intents.SMS_RECEIVED_ACTION; @SuppressLint("InlinedApi") private static final String ACTION_SMS_NEW = Telephony.Sms.Intents.SMS_DELIVER_ACTION; /** * Intent.action for receiving MMS. */ @SuppressLint("InlinedApi") private static final String ACTION_MMS_OLD = Telephony.Sms.Intents.WAP_PUSH_RECEIVED_ACTION; @SuppressLint("InlinedApi") private static final String ACTION_MMS_MEW = Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION; /** * An unreadable MMS body. */ private static final String MMS_BODY = "<MMS>"; /** * Index: thread id. */ private static final int ID_TID = 0; /** * Index: count. */ private static final int ID_COUNT = 1; /** * Sort the newest message first. */ private static final String SORT = Calls.DATE + " DESC"; /** * Delay for spinlock, waiting for new messages. */ private static final long SLEEP = 500; /** * Number of maximal spins. */ private static final int MAX_SPINS = 15; /** * ID for new message notification. */ private static final int NOTIFICATION_ID_NEW = 1; /** * Last unread message's date. */ private static long lastUnreadDate = 0L; /** * Last unread message's body. */ private static String lastUnreadBody = null; /** * Red lights. */ static final int RED = 0xFFFF0000; @Override public final void onReceive(final Context context, final Intent intent) { if (SMSdroid.isDefaultApp(context)) { handleOnReceive(this, context, intent); } } @SuppressLint("NewApi") private static boolean shouldHandleSmsAction(final Context context, final String action) { return ACTION_SMS_NEW.equals(action) // -> is >= android 4.4 and default app || ACTION_SMS_OLD.equals(action) && ( // handle old action only if: Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT // -> is < android 4.4 || !BuildConfig.APPLICATION_ID // or not default app .equals(Telephony.Sms.getDefaultSmsPackage(context))); } static void handleOnReceive(final BroadcastReceiver receiver, final Context context, final Intent intent) { final String action = intent.getAction(); Log.d(TAG, "onReceive(context, ", action, ")"); final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); final PowerManager.WakeLock wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); wakelock.acquire(); Log.i(TAG, "got wakelock"); Log.d(TAG, "got intent: ", action); try { Log.d(TAG, "sleep(", SLEEP, ")"); Thread.sleep(SLEEP); } catch (InterruptedException e) { Log.d(TAG, "interrupted in spinlock", e); e.printStackTrace(); } String text; if (SenderActivity.MESSAGE_SENT_ACTION.equals(action)) { handleSent(context, intent, receiver.getResultCode()); } else { boolean silent = false; if (shouldHandleSmsAction(context, action)) { Bundle b = intent.getExtras(); assert b != null; Object[] messages = (Object[]) b.get("pdus"); SmsMessage[] smsMessage = new SmsMessage[messages.length]; int l = messages.length; for (int i = 0; i < l; i++) { smsMessage[i] = SmsMessage.createFromPdu((byte[]) messages[i]); } text = null; if (l > 0) { // concatenate multipart SMS body StringBuilder sbt = new StringBuilder(); for (int i = 0; i < l; i++) { sbt.append(smsMessage[i].getMessageBody()); } text = sbt.toString(); // ! Check in blacklist db - filter spam String s = smsMessage[0].getDisplayOriginatingAddress(); // this code is used to strip a forwarding agent and display the orginated number as sender final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if (prefs.getBoolean(PreferencesActivity.PREFS_FORWARD_SMS_CLEAN, false) && text.contains(":")) { Pattern smsPattern = Pattern.compile("([0-9a-zA-Z+]+):"); Matcher m = smsPattern.matcher(text); if (m.find()) { s = m.group(1); Log.d(TAG, "found forwarding sms number: (", s, ")"); // now strip the sender from the message Pattern textPattern = Pattern.compile("^[0-9a-zA-Z+]+: (.*)"); Matcher m2 = textPattern.matcher(text); if (text.contains(":") && m2.find()) { text = m2.group(1); Log.d(TAG, "stripped the message"); } } } final SpamDB db = new SpamDB(context); db.open(); if (db.isInDB(smsMessage[0].getOriginatingAddress())) { Log.d(TAG, "Message from ", s, " filtered."); silent = true; } else { Log.d(TAG, "Message from ", s, " NOT filtered."); } db.close(); if (action.equals(ACTION_SMS_NEW)) { // API19+: save message to the database ContentValues values = new ContentValues(); values.put("address", s); values.put("body", text); context.getContentResolver().insert(Uri.parse("content://sms/inbox"), values); Log.d(TAG, "Insert SMS into database: ", s, ", ", text); } } updateNotificationsWithNewText(context, text, silent); } else if (ACTION_MMS_OLD.equals(action) || ACTION_MMS_MEW.equals(action)) { text = MMS_BODY; // TODO API19+ MMS code updateNotificationsWithNewText(context, text, silent); } } wakelock.release(); Log.i(TAG, "wakelock released"); } private static void updateNotificationsWithNewText(final Context context, final String text, final boolean silent) { if (silent) { Log.i(TAG, "ignore notifications for silent text"); return; } Log.d(TAG, "text: ", text); int count = MAX_SPINS; do { Log.d(TAG, "spin: ", count); try { Log.d(TAG, "sleep(", SLEEP, ")"); Thread.sleep(SLEEP); } catch (InterruptedException e) { Log.d(TAG, "interrupted in spin lock", e); e.printStackTrace(); } --count; } while (updateNewMessageNotification(context, text) <= 0 && count > 0); if (count == 0) { // use messages as they are available updateNewMessageNotification(context, null); } } /** * Get unread SMS. * * @param cr {@link ContentResolver} to query * @param text text of the last assumed unread message * @return [thread id (-1 if there are more), number of unread messages (-1 if text does not * match newest message)] */ private static int[] getUnreadSMS(final ContentResolver cr, final String text) { Log.d(TAG, "getUnreadSMS(cr, ", text, ")"); Cursor cursor = cr.query(URI_SMS, Message.PROJECTION, Message.SELECTION_READ_UNREAD, Message.SELECTION_UNREAD, SORT); //Cursor cursor = cr.query(URI_SMS, null, null, null, null); if (cursor == null || cursor.isClosed() || cursor.getCount() == 0 || !cursor.moveToFirst()) { if (text != null) { // try again! if (cursor != null && !cursor.isClosed()) { cursor.close(); } return new int[] { -1, -1 }; } else { if (cursor != null && !cursor.isClosed()) { cursor.close(); } return new int[] { 0, 0 }; } } final String t = cursor.getString(Message.INDEX_BODY); if (text != null && (t == null || !t.startsWith(text))) { if (!cursor.isClosed()) { cursor.close(); } return new int[] { -1, -1 }; // try again! } final long d = cursor.getLong(Message.INDEX_DATE); if (d > lastUnreadDate) { lastUnreadDate = d; lastUnreadBody = t; } int tid = cursor.getInt(Message.INDEX_THREADID); while (cursor.moveToNext() && tid > -1) { // check if following messages are from the same thread if (tid != cursor.getInt(Message.INDEX_THREADID)) { tid = -1; } } final int count = cursor.getCount(); if (!cursor.isClosed()) { cursor.close(); } return new int[] { tid, count }; } /** * Get unread MMS. * * @param cr {@link ContentResolver} to query * @param text text of the last assumed unread message * @return [thread id (-1 if there are more), number of unread messages] */ private static int[] getUnreadMMS(final ContentResolver cr, final String text) { Log.d(TAG, "getUnreadMMS(cr, ", text, ")"); Cursor cursor = cr.query(URI_MMS, Message.PROJECTION_READ, Message.SELECTION_READ_UNREAD, Message.SELECTION_UNREAD, null); if (cursor == null || cursor.isClosed() || cursor.getCount() == 0 || !cursor.moveToFirst()) { if (MMS_BODY.equals(text)) { if (cursor != null && !cursor.isClosed()) { cursor.close(); } return new int[] { -1, -1 }; // try again! } else { if (cursor != null && !cursor.isClosed()) { cursor.close(); } return new int[] { 0, 0 }; } } int tid = cursor.getInt(Message.INDEX_THREADID); long d = cursor.getLong(Message.INDEX_DATE); if (d < ConversationListActivity.MIN_DATE) { d *= ConversationListActivity.MILLIS; } if (d > lastUnreadDate) { lastUnreadDate = d; lastUnreadBody = null; } while (cursor.moveToNext() && tid > -1) { // check if following messages are from the same thread if (tid != cursor.getInt(Message.INDEX_THREADID)) { tid = -1; } } final int count = cursor.getCount(); if (!cursor.isClosed()) { cursor.close(); } return new int[] { tid, count }; } /** * Get unread messages (MMS and SMS). * * @param cr {@link ContentResolver} to query * @param text text of the last assumed unread message * @return [thread id (-1 if there are more), number of unread messages (-1 if text does not * match newest message)] */ private static int[] getUnread(final ContentResolver cr, final String text) { try { Log.d(TAG, "getUnread(cr, ", text, ")"); lastUnreadBody = null; lastUnreadDate = 0L; String t = text; if (MMS_BODY.equals(t)) { t = null; } final int[] retSMS = getUnreadSMS(cr, t); if (retSMS[ID_COUNT] == -1) { // return to retry return new int[] { -1, -1 }; } final int[] retMMS = getUnreadMMS(cr, text); if (retMMS[ID_COUNT] == -1) { // return to retry return new int[] { -1, -1 }; } final int[] ret = new int[] { -1, retSMS[ID_COUNT] + retMMS[ID_COUNT] }; if (retMMS[ID_TID] <= 0 || retSMS[ID_TID] == retMMS[ID_TID]) { ret[ID_TID] = retSMS[ID_TID]; } else if (retSMS[ID_TID] <= 0) { ret[ID_TID] = retMMS[ID_TID]; } return ret; } catch (SQLiteException e) { Log.e(TAG, "unable to get unread messages", e); return new int[] { -1, 0 }; } } /** * Update new message {@link Notification}. * * @param context {@link Context} * @param text text of the last assumed unread message * @return number of unread messages */ static int updateNewMessageNotification(final Context context, final String text) { Log.d(TAG, "updNewMsgNoti(", context, ",", text, ")"); final NotificationManager mNotificationMgr = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final boolean enableNotifications = prefs.getBoolean(PreferencesActivity.PREFS_NOTIFICATION_ENABLE, true); final boolean privateNotification = prefs.getBoolean(PreferencesActivity.PREFS_NOTIFICATION_PRIVACY, false); final boolean showPhoto = !privateNotification && prefs.getBoolean(PreferencesActivity.PREFS_CONTACT_PHOTO, true); if (!enableNotifications) { mNotificationMgr.cancelAll(); Log.d(TAG, "no notification needed!"); } final int[] status = getUnread(context.getContentResolver(), text); final int l = status[ID_COUNT]; final int tid = status[ID_TID]; // FIXME l is always -1.. Log.d(TAG, "l: ", l); if (l < 0) { return l; } if (enableNotifications && (text != null || l == 0)) { mNotificationMgr.cancel(NOTIFICATION_ID_NEW); } Uri uri; PendingIntent pIntent; if (l == 0) { final Intent i = new Intent(context, ConversationListActivity.class); // add pending intent i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); pIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT); } else { final NotificationCompat.Builder nb = new NotificationCompat.Builder(context); boolean showNotification = true; Intent i; if (tid >= 0) { uri = Uri.parse(MessageListActivity.URI + tid); i = new Intent(Intent.ACTION_VIEW, uri, context, MessageListActivity.class); pIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); if (enableNotifications) { final Conversation conv = Conversation.getConversation(context, tid, true); if (conv != null) { String a; if (privateNotification) { if (l == 1) { a = context.getString(R.string.new_message_); } else { a = context.getString(R.string.new_messages_); } } else { a = conv.getContact().getDisplayName(); } showNotification = true; nb.setSmallIcon(PreferencesActivity.getNotificationIcon(context)); nb.setTicker(a); nb.setWhen(lastUnreadDate); if (l == 1) { String body; if (privateNotification) { body = context.getString(R.string.new_message); } else { body = lastUnreadBody; } if (body == null) { body = context.getString(R.string.mms_conversation); } nb.setContentTitle(a); nb.setContentText(body); nb.setContentIntent(pIntent); // add long text nb.setStyle(new NotificationCompat.BigTextStyle().bigText(body)); // add actions Intent nextIntent = new Intent(NotificationBroadcastReceiver.ACTION_MARK_READ); nextIntent.putExtra(NotificationBroadcastReceiver.EXTRA_MURI, uri.toString()); PendingIntent nextPendingIntent = PendingIntent.getBroadcast(context, 0, nextIntent, PendingIntent.FLAG_UPDATE_CURRENT); nb.addAction(R.drawable.ic_menu_mark, context.getString(R.string.mark_read_), nextPendingIntent); nb.addAction(R.drawable.ic_menu_compose, context.getString(R.string.reply), pIntent); } else { nb.setContentTitle(a); nb.setContentText(context.getString(R.string.new_messages, l)); nb.setContentIntent(pIntent); } if (showPhoto // just for the speeeeed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { try { conv.getContact().update(context, false, true); } catch (NullPointerException e) { Log.e(TAG, "updating contact failed", e); } Drawable d = conv.getContact().getAvatar(context, null); if (d instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDrawable) d).getBitmap(); // 24x24 dp according to android iconography -> // http://developer.android.com/design/style/iconography.html#notification int px = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64, context.getResources().getDisplayMetrics())); nb.setLargeIcon(Bitmap.createScaledBitmap(bitmap, px, px, false)); } } } } } else { uri = Uri.parse(MessageListActivity.URI); i = new Intent(Intent.ACTION_VIEW, uri, context, ConversationListActivity.class); pIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); if (enableNotifications) { showNotification = true; nb.setSmallIcon(PreferencesActivity.getNotificationIcon(context)); nb.setTicker(context.getString(R.string.new_messages_)); nb.setWhen(lastUnreadDate); nb.setContentTitle(context.getString(R.string.new_messages_)); nb.setContentText(context.getString(R.string.new_messages, l)); nb.setContentIntent(pIntent); nb.setNumber(l); } } // add pending intent i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); if (enableNotifications && showNotification) { int[] ledFlash = PreferencesActivity.getLEDflash(context); nb.setLights(PreferencesActivity.getLEDcolor(context), ledFlash[0], ledFlash[1]); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); if (text != null) { final boolean vibrate = p.getBoolean(PreferencesActivity.PREFS_VIBRATE, false); final String s = p.getString(PreferencesActivity.PREFS_SOUND, null); Uri sound; if (s == null || s.length() <= 0) { sound = null; } else { sound = Uri.parse(s); } if (vibrate) { final long[] pattern = PreferencesActivity.getVibratorPattern(context); if (pattern.length == 1 && pattern[0] == 0) { nb.setDefaults(Notification.DEFAULT_VIBRATE); } else { nb.setVibrate(pattern); } } nb.setSound(sound); } } Log.d(TAG, "uri: ", uri); mNotificationMgr.cancel(NOTIFICATION_ID_NEW); if (enableNotifications && showNotification) { try { mNotificationMgr.notify(NOTIFICATION_ID_NEW, nb.getNotification()); } catch (IllegalArgumentException e) { Log.e(TAG, "illegal notification: ", nb, e); } } } Log.d(TAG, "return ", l, " (2)"); //noinspection ConstantConditions AppWidgetManager.getInstance(context).updateAppWidget(new ComponentName(context, WidgetProvider.class), WidgetProvider.getRemoteViews(context, l, pIntent)); return l; } /** * Update failed message notification. * * @param context {@link Context} * @param uri {@link Uri} to message */ private static void updateFailedNotification(final Context context, final Uri uri) { Log.d(TAG, "updateFailedNotification: ", uri); final Cursor c = context.getContentResolver().query(uri, Message.PROJECTION_SMS, null, null, null); if (c != null && c.moveToFirst()) { final int id = c.getInt(Message.INDEX_ID); final int tid = c.getInt(Message.INDEX_THREADID); final String body = c.getString(Message.INDEX_BODY); final long date = c.getLong(Message.INDEX_DATE); Conversation conv = Conversation.getConversation(context, tid, true); final NotificationManager mNotificationMgr = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context); final boolean privateNotification = p.getBoolean(PreferencesActivity.PREFS_NOTIFICATION_PRIVACY, false); Intent intent; if (conv == null) { intent = new Intent(Intent.ACTION_VIEW, null, context, SenderActivity.class); } else { intent = new Intent(Intent.ACTION_VIEW, conv.getUri(), context, MessageListActivity.class); } intent.putExtra(Intent.EXTRA_TEXT, body); String title = context.getString(R.string.error_sending_failed); final int[] ledFlash = PreferencesActivity.getLEDflash(context); final NotificationCompat.Builder b = new NotificationCompat.Builder(context) .setSmallIcon(android.R.drawable.stat_sys_warning).setTicker(title).setWhen(date) .setAutoCancel(true).setLights(RED, ledFlash[0], ledFlash[1]).setContentIntent( PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); String text; if (privateNotification) { title += "!"; text = ""; } else if (conv == null) { title += "!"; text = body; } else { title += ": " + conv.getContact().getDisplayName(); text = body; } b.setContentTitle(title); b.setContentText(text); final String s = p.getString(PreferencesActivity.PREFS_SOUND, null); if (!TextUtils.isEmpty(s)) { b.setSound(Uri.parse(s)); } final boolean vibrate = p.getBoolean(PreferencesActivity.PREFS_VIBRATE, false); if (vibrate) { final long[] pattern = PreferencesActivity.getVibratorPattern(context); if (pattern.length > 1) { b.setVibrate(pattern); } } mNotificationMgr.notify(id, b.build()); } if (c != null && !c.isClosed()) { c.close(); } } /** * Handle sent message. * * @param context {@link Context} * @param intent {@link Intent} * @param resultCode message status */ private static void handleSent(final Context context, final Intent intent, final int resultCode) { final Uri uri = intent.getData(); Log.d(TAG, "sent message: ", uri, ", rc: ", resultCode); if (uri == null) { Log.w(TAG, "handleSent(null)"); return; } if (resultCode == Activity.RESULT_OK) { final ContentValues cv = new ContentValues(1); cv.put(SenderActivity.TYPE, Message.SMS_OUT); context.getContentResolver().update(uri, cv, null, null); } else { updateFailedNotification(context, uri); } } }