/**
* Copyright (C) 2013 Jonathan Gillett, Joseph Heron
*
* 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 com.tinfoil.sms.utility;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.strippedcastle.crypto.InvalidCipherTextException;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Telephony;
import android.telephony.SmsManager;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import com.bugsense.trace.BugSenseHandler;
import com.tinfoil.sms.R;
import com.tinfoil.sms.crypto.Encryption;
import com.tinfoil.sms.crypto.ExchangeKey;
import com.tinfoil.sms.dataStructures.Entry;
import com.tinfoil.sms.dataStructures.Message;
import com.tinfoil.sms.dataStructures.Number;
import com.tinfoil.sms.dataStructures.TrustedContact;
import com.tinfoil.sms.dataStructures.User;
import com.tinfoil.sms.database.DBAccessor;
import com.tinfoil.sms.database.InvalidDatabaseStateException;
import com.tinfoil.sms.messageQueue.MessageBroadcastReciever;
import com.tinfoil.sms.settings.EditNumber;
import com.tinfoil.sms.settings.QuickPrefsActivity;
import com.tinfoil.sms.sms.ConversationView;
import com.tinfoil.sms.sms.KeyExchangeManager;
/**
* An abstract class used to retrieve contacts information from the native
* database and format the data going into tinfoil-sms's database.
*/
public abstract class SMSUtility {
private static final Pattern phoneNumber = Pattern.compile("^[+]1.{10}");
private static final Pattern numOnly = Pattern.compile("\\W");
private static final String numberPattern = "\\d+";
private static final String smallNumberPattern = "\\d{1,9}";
private static final SmsManager sms = SmsManager.getDefault();
public static final String SENT = "content://sms/sent";
public static String NUMBER = "com.tinfoil.sms.number";
public static String MESSAGE = "com.tinfoil.sms.message";
public static String ID = "com.tinfoil.sms.id";
public static final int ENCRYPTED_MESSAGE_LENGTH = 128;
public static final int MESSAGE_LENGTH = 160;
public static final int LIMIT = 50;
public static final boolean saveMessage = false;
/* The encryption engine used to process messages */
public static Encryption cryptoEngine;
public static User user;
private static MessageBroadcastReciever MS = new MessageBroadcastReciever();
/**
* Create an array of Strings to display for the auto-complete
*
* @param tc All the TrustedContacts
* @return A list of all the contacts and their numbers for the auto
* complete list.
*/
public static List<String> contactDisplayMaker(final List<TrustedContact> tc)
{
final List<String> contacts = new ArrayList<String>();
for (int i = 0; i < tc.size(); i++)
{
for (int j = 0; j < tc.get(i).getNumber().size(); j++)
{
contacts.add(tc.get(i).getName() + ", " + tc.get(i).getNumber(j));
}
}
return contacts;
}
/**
* Removes the preceding '1' or '+1' for the given number
*
* @param number The number of the contact
* @return The number without the preceding '1' or '+1'
*/
public static String format(String number)
{
if (number.matches("^1.{10}"))
{
number = number.substring(1);
}
else if (number.matches(phoneNumber.pattern()))
{
number = number.substring(2);
}
number = number.replaceAll(numOnly.pattern(), "");
return number;
}
/**
* Ensure that the user's information is in memory.
* @param dba The database accessor to retrieve the user's key if necessary
* @param user The user stored in memory.
* @return The user that is not null
* @throws InvalidDatabaseStateException If the user's keys are not found then they have
* been deleted thus making any key exchange prior invalid.
*/
public static User getUser(DBAccessor dba, User user) throws InvalidDatabaseStateException
{
if(user == null)
{
user = dba.getUserRow();
if (user == null)
{
throw new InvalidDatabaseStateException("User's keys not set in database");
//user = new User();
//dba.setUser(user);
}
}
return user;
}
public static void handleKeyExchange(ExchangeKey keyThread, final DBAccessor dba,
final Activity activity, String number)
{
/*ExchangeKey.keyDialog = ProgressDialog.show(this, "Exchanging Keys",
"Exchanging. Please wait...", true, false);*/
if (!dba.isTrustedContact(SMSUtility.format
(number)))
{
final Entry entry = dba.getKeyExchangeMessage(number);
//Resolving a key exchange
if (entry != null)
{
final TrustedContact tc = dba.getRow(number);
final Number numberO = tc.getNumber(number);
final String name = tc.getName();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setMessage(activity.getString(R.string.key_exchange_dialog_message)
+ " " + tc.getName() + ", " + numberO.getNumber() + "?")
.setCancelable(true)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//Save the shared secrets
if(SMSUtility.checksharedSecret(numberO.getSharedInfo1()) &&
SMSUtility.checksharedSecret(numberO.getSharedInfo2()))
{
KeyExchangeManager.respondKeyExchangeMessage(activity, numberO, entry);
}
else
{
KeyExchangeManager.requestSharedSecrets(activity, numberO, name, entry);
}}})
.setNegativeButton(android.R.string.no,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
// Cancel the key exchange
Toast.makeText(activity, activity
.getString(R.string.key_exchange_cancelled), Toast.LENGTH_LONG).show();
// Delete key exchange
dba.deleteKeyExchangeMessage(numberO.getNumber());
if(dba.getKeyExchangeMessageCount() == 0)
{
MessageService.mNotificationManager.cancel(MessageService.KEY);
}
}
});
AlertDialog alert = builder.create();
//ExchangeKey.keyDialog.dismiss();
alert.show();
}
else
{
// TODO Show the tutorial for when key exchange sent, need to set listener for keythread
// Walkthrough.show(Step.KEY_SENT, activity)
//Initiate the key exchange with the contact.
keyThread.startThread(activity, SMSUtility.format(number), null);
}
}
else
{
//Untrust the contact.
keyThread.startThread(activity, null, SMSUtility.format(number));
}
}
/**
* Sends the given message to the phone with the given number
*
* @param number The number of the phone that the message is sent to
* @param message The message, encrypted that will be sent to the contact
*/
public static void sendSMS(final Context c, Entry message)
{
final String SENT = "SMS_SENT";
c.registerReceiver(MS, new IntentFilter(SENT));
ArrayList<String> messageList = sms.divideMessage(message.getMessage());
ArrayList<PendingIntent> sentPIList = new ArrayList<PendingIntent>();
for (int i = 0; i < messageList.size(); i++)
{
Intent intent = new Intent(SENT);
intent.putExtra(NUMBER, message.getNumber());
intent.putExtra(MESSAGE, messageList.get(i));
intent.putExtra(ID, message.getId());
sentPIList.add(PendingIntent.getBroadcast(c, 0,
intent, PendingIntent.FLAG_CANCEL_CURRENT));
}
if(messageList.size() > 0) {
sms.sendMultipartTextMessage(message.getNumber(), null, messageList, sentPIList, null);
}
c.unregisterReceiver(MS);
}
/**
* Drops the message into the in-box of the default SMS program. Tricks the
* in-box to think the message was send by the original sender. If the
* user's settings are such to prevent messages from being saved to the
* native sms client as well the method will do nothing, there is NO need to
* check the user's settings outside.
*
* @param c The context of the message sending
* @param srcNumber The number of the contact that sent the message
* @param decMessage The message sent from the contact
* @param dest The folder in the android database that the message
* will be stored in
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void sendToSelf(final Context c, final String srcNumber, final String decMessage, final String dest) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
//Prevent message from doing to native client given user settings
if (ConversationView.sharedPrefs.getBoolean(
QuickPrefsActivity.NATIVE_SAVE_SETTING_KEY, false))
{
final ContentValues values = new ContentValues();
values.put("address", srcNumber);
values.put("body", decMessage);
//Stops native sms client from reading messages as new.
values.put("read", true);
values.put("seen", true);
/* Sets used to determine who sent the message,
* if type == 2 then it is sent from the user
* if type == 1 it has been sent by the contact
*/
if (dest.equalsIgnoreCase(SENT))
{
values.put("type", "2");
}
else
{
values.put("type", "1");
}
c.getContentResolver().insert(Uri.parse(dest), values);
}
}
}
/**
* Sends a message as encrypted or plain text based on the contact's state.
* @param context The context of the class
* @param number The number the text message is being sent to
* @param text The text message
*
* @return boolean whether the message sent or not
*/
public static boolean sendMessage(DBAccessor dba, final Context context, Entry message) {
try
{
if (dba.isTrustedContact(message.getNumber()) &&
ConversationView.sharedPrefs.getBoolean(
QuickPrefsActivity.ENABLE_SETTING_KEY, true) &&
!message.isExchange())
{
// Initialize the cryptographic engine if null
if (SMSUtility.cryptoEngine == null)
{
SMSUtility.cryptoEngine = new Encryption(SMSUtility.getUser(dba, null));
}
Number number = dba.getNumber(format(message.getNumber()));
Log.v("Before Encryption", message.getMessage());
//Create the an encrypted message
final String encrypted = SMSUtility.cryptoEngine.encrypt(number, message.getMessage());
Log.v("After Encrypted", encrypted);
sendSMS(context, new Entry(message.getNumber(), encrypted,
message.getId(), message.getExchange()));
dba.updateEncryptNonce(number);
if(ConversationView.sharedPrefs.getBoolean(
QuickPrefsActivity.SHOW_ENCRYPT_SETTING_KEY, false))
{
sendToSelf(context, message.getNumber(), encrypted, ConversationView.SENT);
dba.addNewMessage(new Message (encrypted, true, Message.SENT_ENCRYPTED),
message.getNumber(), false);
}
sendToSelf(context, message.getNumber(), message.getMessage(), ConversationView.SENT);
//dba.addNewMessage(new Message(message.getMessage(), true, Message.SENT_ENCRYPTED), message.getNumber(), false);
Toast.makeText(context, R.string.encrypted_message_sent, Toast.LENGTH_SHORT).show();
}
else
{
//Sending a plain text message
sendSMS(context, message);
sendToSelf(context, message.getNumber(), message.getMessage(), ConversationView.SENT);
/*if(!message.isExchange())
{
dba.addNewMessage(new Message(message.getMessage(), true, Message.SENT_DEFAULT), message.getNumber(), true);
}*/
Toast.makeText(context, R.string.message_sent, Toast.LENGTH_SHORT).show();
}
return true;
}
catch (InvalidCipherTextException e)
{
Toast.makeText(context, R.string.failed_to_encrypt, Toast.LENGTH_LONG).show();
e.printStackTrace();
BugSenseHandler.sendExceptionMessage("Type", "Encrypt Message Error", e);
return false;
}
catch (final Exception e)
{
Toast.makeText(context, R.string.failed_to_sent, Toast.LENGTH_LONG).show();
e.printStackTrace();
BugSenseHandler.sendExceptionMessage("Type", "Send Message Error", e);
return false;
}
}
/**
* Identifies if the given String is a valid Long
*
* @param number A number in string format
* @return Whether the given String is a valid Long number.
*/
public static boolean isANumber(final String number)
{
if(number != null && number.matches(numberPattern))
{
return true;
}
return false;
}
public static boolean isASmallNumber(final String number)
{
if(number.matches(smallNumberPattern))
{
return true;
}
return false;
}
/**
* Check if the sd card is writable able.
* @return Whether the sd card is writable or not.
*/
public static boolean isMediaWritable()
{
String state = Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state))
{
//Read and Writeable
return true;
}
return false;
}
/**
* Check if the sd card is available.
* @return Whether the sd card is available or not
*/
public static boolean isMediaAvailable()
{
String state = Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state))
{
// Media is able to be read from.
return true;
}
return false;
}
/**
* Parse an auto-complete entry of a 'name, number'
* @param entry The entry that contains the 'name, number'
* @return A string array with the first element being the contact's name
* and the second element being the contact's number. If the given string
* only contains the contact's number the first element will be null.
*/
public static String[] parseAutoComplete(String entry)
{
String[] info = entry.split(", ");
if(info != null)
{
if(info.length == 2 && info[1] != null)
{
if(isANumber(info[1]))
{
return info;
}
}
else
{
if(isANumber(entry))
{
return new String[]{null, entry};
}
}
}
return null;
}
/**
* Checks if the given shared secret is valid
* @param secret The shared secret
* @return Whether the secret is valid or not
*/
public static boolean checksharedSecret(String secret)
{
if (secret != null && secret.length() >= EditNumber.SHARED_INFO_MIN &&
secret.length() <= EditNumber.SHARED_INFO_MAX)
{
return true;
}
return false;
}
/**
* Checks the two given strings.
* @param original The original string
* @param updated The updated string
* @return If the 2 strings are the same return true, otherwise false
*/
public static boolean isChanged(String original, String updated)
{
if(original != null && updated != null && original.equals(updated))
{
return true;
}
return false;
}
public static void setKeyExchangeTypeface(TextView tv)
{
if ((tv.getTypeface() != null) && tv.getTypeface().isBold())
{
tv.setTypeface(null, Typeface.BOLD_ITALIC);
}
else
{
tv.setTypeface(null, Typeface.ITALIC);
}
}
public static void addMessageToDB(DBAccessor dba, final String number, final String text)
{
if(dba.isTrustedContact(number))
{
dba.addNewMessage(new Message(text, true,
Message.SENT_ENCRYPTED), number, false);
}
else
{
dba.addNewMessage(new Message(text, true,
Message.SENT_DEFAULT), number, false);
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean checkDefault(Context context)
{
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
{
final String myPackageName = context.getPackageName();
if (!myPackageName.equals(Telephony.Sms.getDefaultSmsPackage(context))) {
return false;
}
}
return true;
}
}