de.ub0r.android.websms.WebSMSReceiver.java Source code

Java tutorial

Introduction

Here is the source code for de.ub0r.android.websms.WebSMSReceiver.java

Source

/*
 * Copyright (C) 2010-2011 Felix Bechstein
 * 
 * This file is part of WebSMS.
 * 
 * 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.websms;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
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.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.Telephony;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

import de.ub0r.android.websms.connector.common.Connector;
import de.ub0r.android.websms.connector.common.ConnectorCommand;
import de.ub0r.android.websms.connector.common.ConnectorSpec;
import de.ub0r.android.websms.connector.common.Log;
import de.ub0r.android.websms.connector.common.Utils;

/**
 * Fetch all incoming Broadcasts and forward them to WebSMS.
 *
 * @author flx
 */
public final class WebSMSReceiver extends BroadcastReceiver {

    /**
     * Tag for debug output.
     */
    private static final String TAG = "bcr";

    /**
     * {@link Uri} for saving messages.
     */
    private static final Uri URI_SMS = Uri.parse("content://sms");

    /**
     * {@link Uri} for saving sent messages.
     */
    private static final Uri URI_SENT = Uri.parse("content://sms/sent");

    /**
     * Projection for getting the id.
     */
    private static final String[] PROJECTION_ID = new String[] { BaseColumns._ID };

    /**
     * Intent's scheme to send sms.
     */
    private static final String INTENT_SCHEME_SMSTO = "smsto";

    /**
     * ACTION for publishing information about sent websms.
     */
    private static final String ACTION_CM_WEBSMS = "de.ub0r.android.callmeter.SAVE_WEBSMS";

    private static final String ACTION_SMSDROID_WEBSMS = "de.ub0r.android.websms.SEND_SUCCESSFUL";

    /**
     * Extra holding uri of sent sms.
     */
    private static final String EXTRA_WEBSMS_URI = "uri";

    /**
     * Extra holding name of connector.
     */
    private static final String EXTRA_WEBSMS_CONNECTOR = "connector";

    /**
     * Vibrate x seconds on send.
     */
    private static final long VIBRATOR_SEND = 100L;

    /**
     * SMS DB: address.
     */
    static final String ADDRESS = "address";
    /** SMS DB: person. */
    // private static final String PERSON = "person";

    /**
     * SMS DB: date.
     */
    private static final String DATE = "date";

    /**
     * SMS DB: read.
     */
    static final String READ = "read";
    /** SMS DB: status. */
    // private static final String STATUS = "status";

    /**
     * SMS DB: type.
     */
    static final String TYPE = "type";

    /**
     * SMS DB: body.
     */
    static final String BODY = "body";

    /**
     * SMS DB: type - sent.
     */
    static final int MESSAGE_TYPE_SENT = 2;

    /**
     * SMS DB: type - draft.
     */
    static final int MESSAGE_TYPE_DRAFT = 3;

    public static final long[] VIBRATE_ON_FAIL_PATTERN = { 0, 100, 100, 100, 100 };

    /**
     * Next notification ID.
     */
    private static int nextNotificationID = 1;

    /**
     * LED color for notification.
     */
    private static final int NOTIFICATION_LED_COLOR = 0xffff0000;

    /**
     * LED blink on (ms) for notification.
     */
    private static final int NOTIFICATION_LED_ON = 500;

    /**
     * LED blink off (ms) for notification.
     */
    private static final int NOTIFICATION_LED_OFF = 2000;

    /**
     * Tag for notification about resending
     */
    private static final String NOTIFICATION_RESENDING_TAG = "resending";

    /**
     * Tag for notification about cancelling a resend
     */
    private static final String NOTIFICATION_CANCELLING_RESEND_TAG = "cancelling_resend";

    private static final long RESEND_DELAY_MS = 5000;

    /**
     * List of ids of messages that should not be resent any more.
     */
    private static List<Long> resendCancelledMsgIds = new ArrayList<Long>();

    /**
     * {@inheritDoc}
     */
    @Override
    public void onReceive(final Context context, final Intent intent) {
        final String action = intent.getAction();
        Log.d(TAG, "action: " + action);
        if (action == null) {
            return;
        }
        if (Connector.ACTION_INFO.equals(action)) {
            WebSMSReceiver.handleInfoAction(context, intent);

        } else if (Connector.ACTION_CAPTCHA_REQUEST.equals(action)) {
            final Intent i = new Intent(context, CaptchaActivity.class);
            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            //noinspection ConstantConditions
            i.putExtras(intent.getExtras());
            context.startActivity(i);

        } else if (Connector.ACTION_CANCEL.equals(action)) {
            WebSMSReceiver.handleCancelAction(context, intent);

        } else if (Connector.ACTION_RESEND.equals(action)) {
            WebSMSReceiver.handleResendAction(context, intent);
        }
    }

    /**
     * Fetch INFO broadcast.
     *
     * @param context context
     * @param intent  intent
     */
    private static void handleInfoAction(final Context context, final Intent intent) {
        final ConnectorSpec specs = new ConnectorSpec(intent);
        final ConnectorCommand command = new ConnectorCommand(intent);

        if (specs.getBundle().isEmpty()) {
            // security check. some other apps may send faulty broadcasts
            return;
        }

        try {
            WebSMS.addConnector(specs, command);
        } catch (Exception e) {
            Log.e(TAG, "error while receiving broadcast", e);
        }
        // save send messages
        if (command.getType() == ConnectorCommand.TYPE_SEND) {
            handleSendCommand(context, specs, command);
        }
    }

    /**
     * Handle result of message sending.
     *
     * @param context context
     * @param specs   {@link de.ub0r.android.websms.connector.common.ConnectorSpec}
     * @param command {@link de.ub0r.android.websms.connector.common.ConnectorCommand}
     */
    static void handleSendCommand(final Context context, final ConnectorSpec specs,
            final ConnectorCommand command) {

        boolean isHandled = false;
        final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context);

        if (!specs.hasStatus(ConnectorSpec.STATUS_ERROR)) {
            // Sent successfully
            saveMessage(context, specs, command, MESSAGE_TYPE_SENT);
            if (p.getBoolean(WebSMS.PREFS_SEND_VIBRATE, false)) {
                final Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
                if (v != null) {
                    v.vibrate(VIBRATOR_SEND);
                    v.cancel();
                }
            }
            isHandled = true;
            messageCompleted(context, command);
        }

        if (!isHandled) {
            // Resend if possible (network might be down temporarily or an odd
            // failure on the provider's web side)
            final int maxResendCount = de.ub0r.android.lib.Utils
                    .parseInt(p.getString(WebSMS.PREFS_MAX_RESEND_COUNT, "0"), 0);
            if (maxResendCount > 0) {
                int wasResendCount = command.getResendCount();

                if (wasResendCount < maxResendCount && !isResendCancelled(command.getMsgId())) {

                    // schedule resend
                    command.setResendCount(wasResendCount + 1);
                    displayResendingNotification(context, command);
                    scheduleMessageResend(context, specs, command);

                    isHandled = true;
                }
            }
        }

        if (!isHandled) {
            // Display notification if sending failed
            displaySendingFailedNotification(context, specs, command);
            messageCompleted(context, command);
        }
    }

    /**
     * Handle cancellation request.
     *
     * @param context context
     * @param intent  intent
     */
    private static void handleCancelAction(final Context context, final Intent intent) {
        final ConnectorCommand command = new ConnectorCommand(intent);
        cancelResend(command.getMsgId());
        displayCancellingResendNotification(context, command);
    }

    /**
     * Handle resend request.
     *
     * @param context context
     * @param intent  intent
     */
    private static void handleResendAction(final Context context, final Intent intent) {

        final ConnectorSpec connector = new ConnectorSpec(intent);
        final ConnectorCommand command = new ConnectorCommand(intent);
        long msgId = command.getMsgId();

        if (!isResendCancelled(msgId)) {
            WebSMS.runCommand(context, connector, command);
        } else {
            displaySendingFailedNotification(context, connector, command);
            messageCompleted(context, command);
        }
    }

    /**
     * Displays notification if sending failed
     *
     * @param context context
     * @param specs   {@link de.ub0r.android.websms.connector.common.ConnectorSpec}
     * @param command {@link de.ub0r.android.websms.connector.common.ConnectorCommand}
     */
    private static void displaySendingFailedNotification(final Context context, final ConnectorSpec specs,
            final ConnectorCommand command) {

        final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(context);

        String to = Utils.joinRecipients(command.getRecipients(), ", ");

        final Intent i = new Intent(Intent.ACTION_SENDTO, Uri.parse(INTENT_SCHEME_SMSTO + ":" + Uri.encode(to)),
                context, WebSMS.class);
        // add pending intent
        i.putExtra(Intent.EXTRA_TEXT, command.getText());
        i.putExtra(WebSMS.EXTRA_ERRORMESSAGE, specs.getErrorMessage());
        i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
        final PendingIntent cIntent = PendingIntent.getActivity(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);

        NotificationCompat.Builder b = new NotificationCompat.Builder(context)
                .setSmallIcon(R.drawable.stat_notify_sms_failed)
                .setContentTitle(context.getString(R.string.notify_failed) + " " + specs.getErrorMessage())
                .setContentText(to + ": " + command.getText()).setTicker(context.getString(R.string.notify_failed_))
                .setWhen(System.currentTimeMillis()).setContentIntent(cIntent).setAutoCancel(true)
                .setLights(NOTIFICATION_LED_COLOR, NOTIFICATION_LED_ON, NOTIFICATION_LED_OFF);

        final String s = p.getString(WebSMS.PREFS_FAIL_SOUND, null);
        if (!TextUtils.isEmpty(s)) {
            b.setSound(Uri.parse(s));
        }

        if (p.getBoolean(WebSMS.PREFS_FAIL_VIBRATE, false)) {
            b.setVibrate(VIBRATE_ON_FAIL_PATTERN);
        }

        NotificationManager mNotificationMgr = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationMgr.notify(getNotificationID(), b.build());

        // show a toast as well
        final String em = specs.getErrorMessage();
        if (em != null) {
            Toast.makeText(context, em, Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Displays (or updates) notification about resending a failed message.
     *
     * @param context context
     * @param command {@link ConnectorCommand}
     */
    private static void displayResendingNotification(final Context context, final ConnectorCommand command) {

        long msgId = command.getMsgId();

        // Clicking on the notification will send a cancellation request
        final Intent i = new Intent(Connector.ACTION_CANCEL);
        command.setToIntent(i);
        PendingIntent pIntent = PendingIntent.getBroadcast(context, (int) msgId, i,
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder b = new NotificationCompat.Builder(context)
                .setSmallIcon(R.drawable.stat_notify_resending)
                .setContentTitle(context.getString(R.string.resending_failed_msg_))
                .setContentText(getResendSummary(context, command)).setContentIntent(pIntent)
                .setTicker(context.getString(R.string.notify_failed_now_resending))
                .setWhen(System.currentTimeMillis()).setOngoing(true);

        NotificationManager mNotificationMgr = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);
        // There might be several messages being resent,
        // so we use msgId to distinguish them
        mNotificationMgr.notify(NOTIFICATION_RESENDING_TAG, (int) msgId, b.build());
    }

    /**
     * Displays notification about cancelling a resend.
     *
     * @param context context
     * @param command {@link ConnectorCommand}
     */
    private static void displayCancellingResendNotification(final Context context, final ConnectorCommand command) {

        long msgId = command.getMsgId();

        // on click, do nothing
        PendingIntent pIntent = PendingIntent.getActivity(context, (int) msgId, new Intent(),
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder b = new NotificationCompat.Builder(context)
                .setSmallIcon(R.drawable.stat_notify_resending)
                .setContentTitle(context.getString(R.string.cancelling_resend))
                .setContentText(getResendSummary(context, command))
                .setTicker(context.getString(R.string.cancelling_resend)).setWhen(System.currentTimeMillis())
                .setOngoing(true).setContentIntent(pIntent);

        NotificationManager mNotificationMgr = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationMgr.cancel(NOTIFICATION_RESENDING_TAG, (int) msgId);
        mNotificationMgr.notify(NOTIFICATION_CANCELLING_RESEND_TAG, (int) msgId, b.build());
    }

    /**
     * Returns a brief description of a resend attempt.
     *
     * @param context context
     * @param command {@link ConnectorCommand}
     * @return description
     */
    private static String getResendSummary(final Context context, final ConnectorCommand command) {
        String to = Utils.joinRecipients(command.getRecipients(), ", ");
        return context.getString(R.string.attempt) + ": " + command.getResendCount() + ", "
                + context.getString(R.string.to) + ": " + to;
    }

    /**
     * Cleans up after a message sending has been completed (successfully or not).
     *
     * @param context context
     * @param command {@link ConnectorCommand}
     */
    private static void messageCompleted(final Context context, final ConnectorCommand command) {
        long msgId = command.getMsgId();

        // clear notification
        NotificationManager mNotificationMgr = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationMgr.cancel(NOTIFICATION_RESENDING_TAG, (int) msgId);
        mNotificationMgr.cancel(NOTIFICATION_CANCELLING_RESEND_TAG, (int) msgId);

        // clear flags
        resendCancelledMsgIds.remove(msgId);
    }

    /**
     * Checks if this message should not be sent any more.
     *
     * @param msgId message id
     * @return cancelled
     */
    private static boolean isResendCancelled(final long msgId) {
        return resendCancelledMsgIds.contains(msgId);
    }

    /**
     * Marks the message as cancelled so that it does not get resent any more.
     *
     * @param msgId message id
     */
    private static void cancelResend(final long msgId) {
        resendCancelledMsgIds.add(msgId);
    }

    /**
     * Get a fresh and unique ID for a new notification.
     *
     * @return return the ID
     */
    private static synchronized int getNotificationID() {
        ++nextNotificationID;
        return nextNotificationID;
    }

    /**
     * Save Message to internal database.
     *
     * @param context {@link android.content.Context}
     * @param specs   {@link de.ub0r.android.websms.connector.common.ConnectorSpec}
     * @param command {@link de.ub0r.android.websms.connector.common.ConnectorCommand}
     * @param msgType sent or draft?
     */
    static void saveMessage(final Context context, final ConnectorSpec specs, final ConnectorCommand command,
            final int msgType) {
        if (command.getType() != ConnectorCommand.TYPE_SEND) {
            return;
        }
        if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(WebSMS.PREFS_DROP_SENT, false)) {
            Log.i(TAG, "drop sent messages");
            return;
        }

        // save message to android's internal sms database
        final ContentResolver cr = context.getContentResolver();
        assert cr != null;
        final ContentValues values = new ContentValues();
        values.put(TYPE, msgType);

        if (msgType == MESSAGE_TYPE_SENT) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

                if (isRealSMS(specs)) {
                    // drop messages from "SMS" connector. it gets saved internally.
                    return;
                }

                try {
                    // API19+ does not allow writing to content://sms anymore
                    // anyway, give it a try, if SMSdroid is not installed
                    // AppOps might let the app write the message
                    if (Telephony.Sms.getDefaultSmsPackage(context).equals("de.ub0r.android.smsdroid")) {
                        sendMessageToSMSdroid(context, specs, command);
                        return;
                    }
                } catch (NullPointerException e) {
                    Log.w(TAG, "there is no telephony service!");
                    // fall back saving the message the old fashion way. it might work..
                }
            }

            final String[] uris = command.getMsgUris();
            if (uris != null && uris.length > 0) {
                for (String s : uris) {
                    final Uri u = Uri.parse(s);
                    try {
                        final int updated = cr.update(u, values, null, null);
                        Log.d(TAG, "updated: " + updated);
                        if (updated > 0 && specs != null && !isRealSMS(specs)) {
                            sendMessageToCallMeter(context, specs, u);
                        }
                    } catch (SQLiteException e) {
                        Log.e(TAG, "error updating sent message: " + u, e);
                        Toast.makeText(context, R.string.log_error_saving_message, Toast.LENGTH_LONG).show();
                    }
                }
                return; // skip legacy saving
            }
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            return; // skip saving drafts on API19+
        }

        final String text = command.getText();

        Log.d(TAG, "save message(s):");
        Log.d(TAG, "type: " + msgType);
        Log.d(TAG, "TEXT: " + text);
        values.put(READ, 1);
        values.put(BODY, text);
        if (command.getSendLater() > 0) {
            values.put(DATE, command.getSendLater());
            Log.d(TAG, "DATE: " + command.getSendLater());
        }
        final String[] recipients = command.getRecipients();
        final ArrayList<String> inserted = new ArrayList<String>(recipients.length);
        for (String recipient : recipients) {
            if (recipient == null || recipient.trim().length() == 0) {
                continue; // skip empty recipients

            }
            String address = Utils.getRecipientsNumber(recipient);
            Log.d(TAG, "TO: " + address);
            try {
                final Cursor c = cr.query(
                        URI_SMS, PROJECTION_ID, TYPE + " = " + MESSAGE_TYPE_DRAFT + " AND " + ADDRESS + " = '"
                                + address + "' AND " + BODY + " like '" + text.replace("'", "_") + "'",
                        null, DATE + " DESC");
                if (c != null && c.moveToFirst()) {
                    final Uri u = URI_SENT.buildUpon().appendPath(c.getString(0)).build();
                    assert u != null;
                    Log.d(TAG, "skip saving draft: " + u);
                    inserted.add(u.toString());
                } else {
                    final ContentValues cv = new ContentValues(values);
                    cv.put(ADDRESS, address);
                    // save sms to content://sms/sent
                    Uri u = cr.insert(URI_SENT, cv);
                    if (u != null) {
                        inserted.add(u.toString());
                        if (msgType == MESSAGE_TYPE_SENT) {
                            // API19+ code may reach this point
                            // SMSdroid is not default app
                            // but message was saved as sent somehow
                            sendMessageToCallMeter(context, specs, u);
                        }
                    }
                }
                if (c != null && !c.isClosed()) {
                    c.close();
                }
            } catch (SQLiteException e) {
                Log.e(TAG, "failed saving message", e);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "failed saving message", e);
                Toast.makeText(context, R.string.log_error_saving_message, Toast.LENGTH_LONG).show();
            }
        }
        if (msgType == MESSAGE_TYPE_DRAFT && inserted.size() > 0) {
            command.setMsgUris(inserted.toArray(new String[inserted.size()]));
        }
    }

    private static boolean isRealSMS(final ConnectorSpec specs) {
        //ConnectorSMS uses the WebSMS's package
        return specs.getPackage().equals("de.ub0r.android.websms");
    }

    private static void sendMessageToSMSdroid(final Context context, final ConnectorSpec specs,
            final ConnectorCommand command) {
        Log.d(TAG, "send broadcast to SMSdroid");
        Intent intent = new Intent(ACTION_SMSDROID_WEBSMS);
        intent.putExtra("address", command.getRecipients());
        intent.putExtra("body", command.getText());
        intent.putExtra("connector_name", specs.getName());
        context.sendBroadcast(intent);
    }

    @SuppressLint("InlinedApi")
    private static void sendMessageToCallMeter(final Context context, final ConnectorSpec specs, final Uri u) {
        Log.d(TAG, "send broadcast to CallMeter3G");
        Intent intent = new Intent(ACTION_CM_WEBSMS);
        intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
        intent.putExtra(EXTRA_WEBSMS_URI, u.toString());
        intent.putExtra(EXTRA_WEBSMS_CONNECTOR, specs.getName().toLowerCase());
        context.sendBroadcast(intent);
    }

    /**
     * Schedules resend of a message.
     *
     * @param context context
     * @param specs   {@link de.ub0r.android.websms.connector.common.ConnectorSpec}
     * @param command {@link de.ub0r.android.websms.connector.common.ConnectorCommand}
     */
    private static void scheduleMessageResend(final Context context, final ConnectorSpec specs,
            final ConnectorCommand command) {

        long msgId = command.getMsgId();

        final Intent resendIntent = new Intent(Connector.ACTION_RESEND);
        command.setToIntent(resendIntent);
        specs.setToIntent(resendIntent);

        AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + RESEND_DELAY_MS,
                PendingIntent.getBroadcast(context, (int) msgId, resendIntent, PendingIntent.FLAG_CANCEL_CURRENT));
    }
}