/*
* Copyright 2011 David Brazdil
*
* 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 uk.ac.cam.db538.cryptosms.data;
import java.util.ArrayList;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.telephony.SmsManager;
import android.util.Log;
import uk.ac.cam.db538.cryptosms.MyApplication;
import uk.ac.cam.db538.cryptosms.R;
import uk.ac.cam.db538.cryptosms.crypto.Encryption;
import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException;
import uk.ac.cam.db538.cryptosms.storage.MessageData;
import uk.ac.cam.db538.cryptosms.storage.StorageFileException;
/*
* Base class for all text messages
*/
public abstract class Message {
// same for all messages
protected static final int LENGTH_HEADER = 1;
protected static final int OFFSET_HEADER = 0;
public static class MessageException extends Exception {
private static final long serialVersionUID = 4922446456153260918L;
/**
* Instantiates a new message exception.
*/
public MessageException() {
super();
}
/**
* Instantiates a new message exception.
*
* @param message the message
*/
public MessageException(String message) {
super(message);
}
}
public static interface MessageSendingListener {
/**
* On all parts sent.
*/
public void onMessageSent();
/**
* On single part sent.
*
* @param index index of the part
*/
public void onPartSent(int index);
/**
* On error.
*
* @param ex the exception
*/
public void onError(Exception ex);
}
// USE ONLY THE TOP 2 BITS!!!
protected static final byte HEADER_TEXT_FIRST = (byte) 0x00; // 00000000
protected static final byte HEADER_TEXT_OTHER = (byte) 0x80; // 10000000
protected static final byte HEADER_HANDSHAKE = (byte) 0x40; // 01000000
protected static final byte HEADER_CONFIRM = (byte) 0xC0; // 11000000
public static enum MessageType {
HANDSHAKE,
CONFIRM,
TEXT,
UNKNOWN,
NONE
}
private static final String SENT_SMS_ACTION = "CRYPTOSMS_SMS_SENT";
private static long mMessageCounter = 0;
protected abstract ArrayList<byte[]> getBytes() throws StorageFileException, MessageException, EncryptionException;
protected abstract void onMessageSent(String phoneNumber) throws StorageFileException;
protected abstract void onPartSent(String phoneNumber, int index) throws StorageFileException;
/**
* Takes the byte arrays created by getBytes() method and sends
* them to the given phone number.
*
* @param phoneNumber the phone number
* @param context the context
* @param listener the listener
* @throws StorageFileException the storage file exception
* @throws MessageException the message exception
* @throws EncryptionException the encryption exception
*/
public void sendSMS(final String phoneNumber, Context context, final MessageSendingListener listener)
throws StorageFileException, MessageException, EncryptionException {
final ArrayList<byte[]> dataSms = getBytes();
// send
int size = dataSms.size();
final boolean[] deliveryConfirms = new boolean[size];
for (int i = 0; i < size; ++i) {
String intentName = SENT_SMS_ACTION + (mMessageCounter++);
final int intentIndex = i;
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Resources res = context.getResources();
context.unregisterReceiver(this);
// check that it arrived OK
switch (getResultCode()) {
case Activity.RESULT_OK:
// notify and save
deliveryConfirms[intentIndex] = true;
try {
onPartSent(phoneNumber, intentIndex);
listener.onPartSent(intentIndex);
} catch (StorageFileException e) {
listener.onError(e);
}
// check we have all
boolean all = true;
for (boolean b : deliveryConfirms)
all = all && b;
if (all) {
try {
Message.this.onMessageSent(phoneNumber);
listener.onMessageSent();
} catch (StorageFileException e) {
listener.onError(e);
}
}
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
listener.onError(new Exception(res.getString(R.string.error_sending_generic)));
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
listener.onError(new Exception(res.getString(R.string.error_sending_no_service)));
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
listener.onError(new Exception(res.getString(R.string.error_sending_null_pdu)));
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
listener.onError(new Exception(res.getString(R.string.error_sending_radio_off)));
break;
default: // ERROR
listener.onError(new Exception(res.getString(R.string.error_sending_unknown)));
break;
}
}
}, new IntentFilter(intentName));
Intent sentIntent = new Intent(intentName);
PendingIntent sentPI = PendingIntent.getBroadcast(
context.getApplicationContext(), 0,
sentIntent, 0);
Log.d(MyApplication.APP_TAG, sentIntent.toString());
byte[] dataPart = new byte[MessageData.LENGTH_MESSAGE];
int lenData = Math.min(dataSms.get(i).length, MessageData.LENGTH_MESSAGE);
int lenRandom = MessageData.LENGTH_MESSAGE - lenData;
System.arraycopy(dataSms.get(i), 0, dataPart, 0, lenData);
System.arraycopy(Encryption.getEncryption().generateRandomData(lenRandom), 0, dataPart, lenData, lenRandom);
// send the data
SmsManager.getDefault().sendDataMessage(phoneNumber, null, MyApplication.getSmsPort(), dataPart, sentPI, null);
}
}
protected static byte getMessageHeader(byte[] data) {
return (byte) (data[OFFSET_HEADER] & 0xC0);
}
/**
* Returns the message type from given data
*
* @param data the data
* @return the message type
*/
public static MessageType getMessageType(byte[] data) {
switch (getMessageHeader(data)) {
case HEADER_HANDSHAKE:
return MessageType.HANDSHAKE;
case HEADER_CONFIRM:
return MessageType.CONFIRM;
case HEADER_TEXT_FIRST:
case HEADER_TEXT_OTHER:
return MessageType.TEXT;
default:
return MessageType.UNKNOWN;
}
}
}