// SMSLib for Java v3
// A Java API library for sending and receiving SMS via a GSM modem
// or other supported gateways.
// Web Site: http://www.smslib.org
//
// Copyright (C) 2002-2012, Thanasis Delenikas, Athens/GREECE.
// SMSLib is distributed under the terms of the Apache License version 2.0
//
// 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 org.smslib.modem;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.ajwcc.pduUtils.gsm3040.Pdu;
import org.ajwcc.pduUtils.gsm3040.PduParser;
import org.ajwcc.pduUtils.gsm3040.PduUtils;
import org.ajwcc.pduUtils.gsm3040.SmsDeliveryPdu;
import org.ajwcc.pduUtils.gsm3040.SmsStatusReportPdu;
import org.smslib.AGateway;
import org.smslib.Contact;
import org.smslib.GatewayException;
import org.smslib.InboundBinaryMessage;
import org.smslib.InboundEncryptedMessage;
import org.smslib.InboundMessage;
import org.smslib.OutboundMessage;
import org.smslib.Phonebook;
import org.smslib.Service;
import org.smslib.StatusReportMessage;
import org.smslib.TimeoutException;
import org.smslib.USSDRequest;
import org.smslib.UnknownMessage;
import org.smslib.InboundMessage.MessageClasses;
import org.smslib.OutboundMessage.FailureCauses;
import org.smslib.OutboundMessage.MessageStatuses;
import org.smslib.helper.Logger;
import org.smslib.modem.athandler.AATHandler;

/**
 * Class representing GSM modems or phones. Extends AGateway with modem specific
 * operations.
 */
public class ModemGateway extends AGateway
{
	/**
	 * Class representing different types of GSM modems / phones.
	 */
	public enum ModemTypes
	{
		/**
		 * Serially connected modem. These modems are connected via a serial
		 * port, either physical or emulated (i.e. USB, IrDA, etc).
		 */
		SERIAL,
		/**
		 * IP connected modem.
		 */
		IP
	}

	public enum IPProtocols
	{
		TEXT, BINARY
	}

	private AModemDriver driver;

	private AATHandler atHandler;

	private String modemDevice;

	private int modemParms;

	private IPProtocols ipProtocol;

	private boolean ipEncryption;

	private long lastKeepLinkOpen;

	private String manufacturer;

	private String model;

	private String simPin, simPin2;

	private String customInitString;

	private String smscNumber;

	private int outMpRefNo;

	private List<List<InboundMessage>> mpMsgList;

	public ModemGateway(ModemTypes myType, String id, String myModemDevice, int myModemParms, String myManufacturer, String myModel)
	{
		super(id);
		init(myType, myModemDevice, myModemParms, myManufacturer, myModel, null);
	}

	public ModemGateway(String id, String myModemDevice, int myModemParms, String myManufacturer, String myModel, AModemDriver myDriver)
	{
		super(id);
		init(null, myModemDevice, myModemParms, myManufacturer, myModel, myDriver);
	}

	private void init(ModemTypes myType, String myModemDevice, int myModemParms, String myManufacturer, String myModel, AModemDriver myDriver)
	{
		setModemDevice(myModemDevice);
		setModemParms(myModemParms);
		setIpProtocol(IPProtocols.BINARY);
		setIpEncryption(false);
		setLastKeepLinkOpen(-1);
		this.manufacturer = myManufacturer;
		this.model = myModel;
		setAttributes(AGateway.GatewayAttributes.SEND | AGateway.GatewayAttributes.RECEIVE | AGateway.GatewayAttributes.BIGMESSAGES | AGateway.GatewayAttributes.WAPSI | AGateway.GatewayAttributes.PORTADDRESSING | AGateway.GatewayAttributes.FLASHSMS | AGateway.GatewayAttributes.DELIVERYREPORTS);
		if (myDriver != null) setDriver(myDriver);
		else
		{
			if (myType == ModemTypes.SERIAL) setDriver(new SerialModemDriver(this, getModemDevice() + ":" + getModemParms()));
			else setDriver(new IPModemDriver(this, getModemDevice() + ":" + getModemParms()));
		}
		setAtHandler(AATHandler.load(this, this.manufacturer, this.model));
		setSimPin("");
		setSimPin2("");
		setSmscNumber("");
		setCustomInitString("");
		this.outMpRefNo = new Random().nextInt();
		if (this.outMpRefNo < 0) this.outMpRefNo *= -1;
		this.outMpRefNo %= 65536;
		this.mpMsgList = new ArrayList<List<InboundMessage>>();
	}

	public void setIpProtocol(IPProtocols myIpProtocol)
	{
		this.ipProtocol = myIpProtocol;
	}

	public IPProtocols getIpProtocol()
	{
		return this.ipProtocol;
	}

	/**
	 * Set the IP modem transmission encryption status
	 * 
	 * @param ipEncryption
	 *            the status of encryption
	 */
	public void setIpEncryption(boolean ipEncryption)
	{
		this.ipEncryption = ipEncryption;
	}

	/**
	 * Check if the encryption of IP modem transmission is enabled
	 * 
	 * @return true if IP encryption is enabled
	 */
	public boolean getIpEncryption()
	{
		return this.ipEncryption;
	}

	@Override
	public void startGateway() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		Logger.getInstance().logInfo("Starting gateway, using " + getATHandler().getDescription() + " AT Handler.", null, getGatewayId());
		getDriver().connect();
		Logger.getInstance().logInfo("Signal level/bit error rate: " + getATHandler().getSignalLevel(), null, getGatewayId());
		Logger.getInstance().logInfo("Network registration: " + getATHandler().getNetworkRegistration(), null, getGatewayId());
		Logger.getInstance().logInfo("Network operator: " + getATHandler().getNetworkOperator(), null, getGatewayId());
		super.startGateway();
		Logger.getInstance().logInfo("Gateway started.", null, getGatewayId());
	}

	@Override
	public void stopGateway() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		Logger.getInstance().logInfo("Stopping gateway...", null, getGatewayId());
		getATHandler().done();
		super.stopGateway();
		getDriver().disconnect();
		Logger.getInstance().logInfo("Gateway stopped.", null, getGatewayId());
	}

	@Override
	public void readMessages(Collection<InboundMessage> msgList, MessageClasses msgClass) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		if (getStatus() != GatewayStatuses.STARTED) return;
		synchronized (getDriver().getSYNCCommander())
		{
			if (getProtocol() == Protocols.PDU) readMessagesPDU(msgList, msgClass, 0);
			else if (getProtocol() == Protocols.TEXT) readMessagesTEXT(msgList, msgClass, 0);
		}
	}

	@Override
	public InboundMessage readMessage(String memLoc, int memIndex) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		Collection<InboundMessage> msgList;
		if (getStatus() != GatewayStatuses.STARTED) return null;
		synchronized (getDriver().getSYNCCommander())
		{
			msgList = new ArrayList<InboundMessage>();
			readMessages(msgList, MessageClasses.ALL);
			for (InboundMessage msg : msgList)
				if ((msg.getMemIndex() == memIndex) && (msg.getMemLocation().equals(memLoc))) return msg;
			return null;
		}
	}

	@Override
	public boolean sendMessage(OutboundMessage msg) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		boolean sendKeepLinkOpen = false;
		if (getStatus() != GatewayStatuses.STARTED) return false;
		if (getLastKeepLinkOpen() == -1) sendKeepLinkOpen = true;
		if (!sendKeepLinkOpen)
		{
			if ((System.currentTimeMillis() - getLastKeepLinkOpen()) > 4000) sendKeepLinkOpen = true;
		}
		synchronized (getDriver().getSYNCCommander())
		{
			if (sendKeepLinkOpen) getAtHandler().keepLinkOpen();
			setLastKeepLinkOpen(System.currentTimeMillis());
			if (getProtocol() == Protocols.PDU) return sendMessagePDU(msg);
			else if (getProtocol() == Protocols.TEXT) return sendMessageTEXT(msg);
			else return false;
		}
	}

	@Override
	public boolean deleteMessage(InboundMessage msg) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		if (getStatus() != GatewayStatuses.STARTED) return false;
		synchronized (getDriver().getSYNCCommander())
		{
			if (msg.getMemIndex() >= 0) return deleteMessage(msg.getMemIndex(), msg.getMemLocation());
			else if ((msg.getMemIndex() == -1) && (msg.getMpMemIndex().length() != 0))
			{
				StringTokenizer tokens = new StringTokenizer(msg.getMpMemIndex(), ",");
				while (tokens.hasMoreTokens())
					deleteMessage(Integer.parseInt(tokens.nextToken()), msg.getMemLocation());
			}
			return true;
		}
	}

	@Override
	public int readPhonebook(Phonebook phonebook) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		int count = 0;
		String locations, location, entries, entry;
		locations = getATHandler().readPhonebookLocations();
		if (locations.length() > 0)
		{
			StringTokenizer tokens = new StringTokenizer(locations, ",");
			while (tokens.hasMoreTokens())
			{
				location = tokens.nextToken().replaceAll("\"", "");
				entries = getATHandler().readPhonebook(location);
				if ((entries == null) || (entries.trim().length() == 0)) continue;
				BufferedReader reader = new BufferedReader(new StringReader(entries));
				entry = reader.readLine().trim();
				while (!entry.equalsIgnoreCase("OK"))
				{
					entry = entry.replaceAll("\\s*\\+CPBR:\\s*", "");
					entry = entry.replaceAll("\"\"", "\" \"");
					entry = entry.replaceAll("\"", "");
					StringTokenizer tokens4 = new StringTokenizer(entry, ",");
					String index = tokens4.nextToken();
					index = (index == null ? "" : index.trim());
					String phone = tokens4.nextToken();
					phone = (phone == null ? "" : phone.trim());
					// TODO: Parse number type.
					String type = tokens4.nextToken();
					type = (type == null ? "" : type.trim());
					// TODO: *end*
					String name = tokens4.nextToken();
					name = (name == null ? "" : name.trim());
					phonebook.getContacts().add(new Contact(name, phone, location, Integer.parseInt(index)));
					count++;
					entry = reader.readLine().trim();
				}
				reader.close();
			}
		}
		return count;
	}

	private boolean deleteMessage(int memIndex, String memLocation) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		return getATHandler().deleteMessage(memIndex, memLocation);
	}

	private boolean sendMessageTEXT(OutboundMessage msg) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		int refNo;
		boolean ok = false;
		refNo = getATHandler().sendMessage(0, "", msg.getRecipient(), msg.getText());
		if (refNo >= 0)
		{
			msg.setGatewayId(getGatewayId());
			msg.setRefNo("" + refNo);
			msg.setDispatchDate(new Date());
			msg.setMessageStatus(MessageStatuses.SENT);
			incOutboundMessageCount();
			ok = true;
		}
		else
		{
			msg.setRefNo(null);
			msg.setDispatchDate(null);
			msg.setMessageStatus(MessageStatuses.FAILED);
			msg.setErrorMessage(driver.getLastErrorText());
		}
		return ok;
	}

	private void readMessagesTEXT(Collection<InboundMessage> msgList, MessageClasses msgClass, int myLimit) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		int i, j, memIndex;
		int limit = (myLimit < 0 ? 0 : myLimit);
		String response, line, tmpLine, msgText, originator, dateStr, refNo;
		BufferedReader reader;
		StringTokenizer tokens;
		InboundMessage msg;
		Calendar cal1 = Calendar.getInstance();
		Calendar cal2 = Calendar.getInstance();
		for (int ml = 0; ml < (getATHandler().getStorageLocations().length() / 2); ml++)
		{
			if (getATHandler().switchStorageLocation(getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2)))
			{
				response = getATHandler().listMessages(msgClass);
				response = response.replaceAll("\\s+OK\\s+", "\nOK");
				reader = new BufferedReader(new StringReader(response));
				for (;;)
				{
					line = reader.readLine();
					if (line == null) break;
					line = line.trim();
					if (line.length() > 0) break;
				}
				while (true)
				{
					if (line == null) break;
					if (line.length() <= 0 || line.equalsIgnoreCase("OK")) break;
					i = line.indexOf(':');
					j = line.indexOf(',');
					memIndex = 0;
					try
					{
						memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
					}
					catch (NumberFormatException e)
					{
						// TODO: What to do here?
						Logger.getInstance().logWarn("Incorrect Memory Index number parsed!", e, getGatewayId());
					}
					tokens = new StringTokenizer(line, ",");
					tokens.nextToken();
					tokens.nextToken();
					tmpLine = "";
					if (Character.isDigit(tokens.nextToken().trim().charAt(0)))
					{
						line = line.replaceAll(",,", ", ,");
						tokens = new StringTokenizer(line, ",");
						tokens.nextToken();
						tokens.nextToken();
						tokens.nextToken();
						refNo = tokens.nextToken();
						tokens.nextToken();
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
						cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
						cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
						cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
						cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal2.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
						cal2.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
						cal2.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal2.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
						cal2.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
						cal2.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
						msg = new StatusReportMessage(refNo, memIndex, getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2), cal1.getTime(), cal2.getTime());
						msg.setGatewayId(getGatewayId());
						Logger.getInstance().logDebug("IN-DTLS: MI:" + msg.getMemIndex(), null, getGatewayId());
						msgList.add(msg);
						incInboundMessageCount();
					}
					else
					{
						line = line.replaceAll(",,", ", ,");
						tokens = new StringTokenizer(line, ",");
						tokens.nextToken();
						tokens.nextToken();
						originator = tokens.nextToken().replaceAll("\"", "");
						tokens.nextToken();
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal1.set(Calendar.YEAR, 2000 + Integer.parseInt(dateStr.substring(0, 2)));
						cal1.set(Calendar.MONTH, Integer.parseInt(dateStr.substring(3, 5)) - 1);
						cal1.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateStr.substring(6, 8)));
						dateStr = tokens.nextToken().replaceAll("\"", "");
						cal1.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateStr.substring(0, 2)));
						cal1.set(Calendar.MINUTE, Integer.parseInt(dateStr.substring(3, 5)));
						cal1.set(Calendar.SECOND, Integer.parseInt(dateStr.substring(6, 8)));
						msgText = "";
						while (true)
						{
							tmpLine = reader.readLine();
							if (tmpLine == null) break;
							if (tmpLine.startsWith("+CMGL")) break;
							if (tmpLine.startsWith("+CMGR")) break;
							msgText += (msgText.length() == 0 ? "" : "\n") + tmpLine;
						}
						msgText = msgText.trim();
						msg = new InboundMessage(cal1.getTime(), originator, msgText, memIndex, getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2));
						msg.setGatewayId(getGatewayId());
						Logger.getInstance().logDebug("IN-DTLS: MI:" + msg.getMemIndex(), null, getGatewayId());
						msgList.add(msg);
						incInboundMessageCount();
					}
					while (true)
					{
						//line = reader.readLine();
						line = ((tmpLine == null || tmpLine.length() == 0) ? reader.readLine() : tmpLine);
						if (line == null) break;
						line = line.trim();
						if (line.length() > 0) break;
					}
					if ((limit > 0) && (msgList.size() == limit)) break;
				}
				reader.close();
			}
		}
	}

	private boolean sendMessagePDU(OutboundMessage msg) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		boolean ok = false;
		List<String> pdus = msg.getPdus(getSmscNumber(), this.outMpRefNo);
		for (String pdu : pdus)
		{
			Pdu newPdu = new PduParser().parsePdu(pdu);
			Logger.getInstance().logDebug(newPdu.toString(), null, getGatewayId());
			int j = pdu.length() / 2;
			if (getSmscNumber() == null)
			{
				// Do nothing on purpose!
			}
			else if (getSmscNumber().length() == 0) j--;
			else
			{
				int smscNumberLen = getSmscNumber().length();
				if (getSmscNumber().charAt(0) == '+') smscNumberLen--;
				if (smscNumberLen % 2 != 0) smscNumberLen++;
				int smscLen = (2 + smscNumberLen) / 2;
				j = j - smscLen - 1;
			}
			Logger.getInstance().logDebug("Sending Pdu: " + pdu, null, getGatewayId());
			int refNo = getATHandler().sendMessage(j, pdu, null, null);
			if (refNo >= 0)
			{
				msg.setGatewayId(getGatewayId());
				msg.setRefNo(String.valueOf(refNo));
				msg.setDispatchDate(new Date());
				msg.setMessageStatus(MessageStatuses.SENT);
				msg.setFailureCause(FailureCauses.NO_ERROR);
				incOutboundMessageCount();
				ok = true;
			}
			else
			{
				msg.setRefNo(null);
				msg.setDispatchDate(null);
				msg.setMessageStatus(MessageStatuses.FAILED);
				msg.setFailureCause(FailureCauses.UNKNOWN);
				ok = false;
				msg.setErrorMessage(driver.getLastErrorText());
				break;
			}
			if (!ok) break;
		}
		if (pdus.size() > 1)
		{
			this.outMpRefNo = (this.outMpRefNo + 1) % 65536;
		}
		return ok;
	}

	private void readMessagesPDU(Collection<InboundMessage> msgList, MessageClasses messageClass, int myLimit) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		int i, j, memIndex;
		String response, line, pduString;
		BufferedReader reader;
		int limit = (myLimit < 0 ? 0 : myLimit);
		for (int ml = 0; ml < (getATHandler().getStorageLocations().length() / 2); ml++)
		{
			if (getATHandler().switchStorageLocation(getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2)))
			{
				response = getATHandler().listMessages(messageClass);
				response = response.replaceAll("\\s+OK\\s+", "\nOK");
				reader = new BufferedReader(new StringReader(response));
				for (;;)
				{
					line = reader.readLine();
					if (line == null) break;
					line = line.trim();
					if (line.length() > 0) break;
				}
				// use the parser to determine the message type
				PduParser parser = new PduParser();
				while (true)
				{
					if (line == null) break;
					line = line.trim();
					if (line.length() <= 0 || line.equalsIgnoreCase("OK")) break;
					if (line.length() <= 0 || line.equalsIgnoreCase("ERROR")) break;
					i = line.indexOf(':');
					j = line.indexOf(',');
					memIndex = 0;
					try
					{
						memIndex = Integer.parseInt(line.substring(i + 1, j).trim());
					}
					catch (NumberFormatException e)
					{
						// TODO: What to do here?
						Logger.getInstance().logWarn("Incorrect Memory Index number parsed!", e, getGatewayId());
					}
					// Start modifications by Wim Stevens
					// A line contains something like +CMGL: 1,1,,25
					// first parameter is the memory index
					// last parameter is the length of the PDU, not counting the addressing part
					// the parser must always have an addressing part -> we will add it if required 
					i = line.lastIndexOf(',');
					j = line.length();
					int pduSize = 0;
					try
					{
						pduSize = Integer.parseInt(line.substring(i + 1, j).trim());
					}
					catch (NumberFormatException e)
					{
						// TODO: What to do here?
						Logger.getInstance().logWarn("Incorrect pdu size parsed!", e, getGatewayId());
					}
					pduString = reader.readLine().trim();
					if ((pduSize > 0) && ((pduSize * 2) == pduString.length()))
					{
						pduString = "00" + pduString;
					}
					try
					{
						Logger.getInstance().logDebug("READ PDU: " + pduString, null, getGatewayId());
						// this will throw an exception for PDUs
						// it can't classify
						Pdu pdu = parser.parsePdu(pduString);
						// NOTE: maybe a message validity vs the current
						//       date should be put here.
						//       if the message is invalid, the message should
						//       be ignored and but logged
						if (pdu instanceof SmsDeliveryPdu)
						{
							Logger.getInstance().logDebug(pdu.toString(), null, getGatewayId());
							InboundMessage msg;
							String memLocation = getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2);
							if (pdu.isBinary())
							{
								msg = new InboundBinaryMessage((SmsDeliveryPdu) pdu, memIndex, memLocation);
								if (Service.getInstance().getKeyManager().getKey(msg.getOriginator()) != null) msg = new InboundEncryptedMessage((SmsDeliveryPdu) pdu, memIndex, memLocation);
							}
							else
							{
								msg = new InboundMessage((SmsDeliveryPdu) pdu, memIndex, memLocation);
							}
							msg.setGatewayId(getGatewayId());
							Logger.getInstance().logDebug("IN-DTLS: MI:" + msg.getMemIndex() + " REF:" + msg.getMpRefNo() + " MAX:" + msg.getMpMaxNo() + " SEQ:" + msg.getMpSeqNo(), null, getGatewayId());
							if (msg.getMpRefNo() == 0)
							{
								// single message
								msgList.add(msg);
								incInboundMessageCount();
							}
							else
							{
								// multi-part message
								int k, l;
								List<InboundMessage> tmpList;
								InboundMessage listMsg;
								boolean found, duplicate;
								found = false;
								for (k = 0; k < this.mpMsgList.size(); k++)
								{
									// List of List<InboundMessage>
									tmpList = this.mpMsgList.get(k);
									listMsg = tmpList.get(0);
									// check if current message list is for this message
									if (listMsg.getMpRefNo() == msg.getMpRefNo())
									{
										duplicate = false;
										// check if the message is already in the message list
										for (l = 0; l < tmpList.size(); l++)
										{
											listMsg = tmpList.get(l);
											if (listMsg.getMpSeqNo() == msg.getMpSeqNo())
											{
												duplicate = true;
												break;
											}
										}
										if (!duplicate) tmpList.add(msg);
										found = true;
										break;
									}
								}
								if (!found)
								{
									// no existing list present for this message
									// add one
									tmpList = new ArrayList<InboundMessage>();
									tmpList.add(msg);
									this.mpMsgList.add(tmpList);
								}
							}
						}
						else if (pdu instanceof SmsStatusReportPdu)
						{
							StatusReportMessage msg;
							msg = new StatusReportMessage((SmsStatusReportPdu) pdu, memIndex, getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2));
							msg.setGatewayId(getGatewayId());
							msgList.add(msg);
							incInboundMessageCount();
						}
						else
						{
							// this theoretically will never happen, but it occasionally does with phones 
							// like some Sony Ericssons (e.g. Z610i, SENT messages are included in this list)
							// instead of throwing a RuntimeException, just ignore any messages that are not of type
							// SmsDeliveryPdu
							// SmsStatusReportPdu
							if (this.displayIllegalReceivedMessages)
							{
								Logger.getInstance().logError("Wrong type of PDU detected: " + pdu.getClass().getName(), null, getGatewayId());
								Logger.getInstance().logError("ERROR PDU: " + pduString, null, getGatewayId());
							}
						}
					}
					catch (Exception e)
					{
						// PduFactory will give an exception
						// for PDUs it can't understand
						UnknownMessage msg;
						msg = new UnknownMessage(pduString, memIndex, getATHandler().getStorageLocations().substring((ml * 2), (ml * 2) + 2));
						msg.setGatewayId(getGatewayId());
						msgList.add(msg);
						incInboundMessageCount();
						Logger.getInstance().logError("Unhandled SMS in inbox, skipping...", e, getGatewayId());
						Logger.getInstance().logError("ERROR PDU: " + pduString, null, getGatewayId());
					}
					while (true)
					{
						line = reader.readLine();
						if (line == null) break;
						line = line.trim();
						if (line.length() > 0) break;
					}
					if ((limit > 0) && (msgList.size() == limit)) break;
				}
				reader.close();
			}
		}
		checkMpMsgList(msgList);
		List<InboundMessage> tmpList;
		for (int k = 0; k < this.mpMsgList.size(); k++)
		{
			tmpList = this.mpMsgList.get(k);
			tmpList.clear();
		}
		mpMsgList.clear();
	}

	private boolean displayIllegalReceivedMessages;

	public void setDisplayIllegalReceivedMessages(boolean b)
	{
		this.displayIllegalReceivedMessages = b;
	}

	public String getMessageByIndex(int msgIndex) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		synchronized (getDriver().getSYNCCommander())
		{
			return getATHandler().getMessageByIndex(msgIndex);
		}
	}

	private void checkMpMsgList(Collection<InboundMessage> msgList) throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		int k, l, m;
		List<InboundMessage> tmpList;
		InboundMessage listMsg, mpMsg;
		boolean found;
		mpMsg = null;
		Logger.getInstance().logDebug("CheckMpMsgList(): MAINLIST: " + this.mpMsgList.size(), null, getGatewayId());
		for (k = 0; k < this.mpMsgList.size(); k++)
		{
			tmpList = this.mpMsgList.get(k);
			Logger.getInstance().logDebug("CheckMpMsgList(): SUBLIST[" + k + "]: " + tmpList.size(), null, getGatewayId());
			listMsg = tmpList.get(0);
			found = false;
			if (listMsg.getMpMaxNo() == tmpList.size())
			{
				found = true;
				for (l = 0; l < tmpList.size(); l++)
					for (m = 0; m < tmpList.size(); m++)
					{
						listMsg = tmpList.get(m);
						if (listMsg.getMpSeqNo() == (l + 1))
						{
							if (listMsg.getMpSeqNo() == 1)
							{
								mpMsg = listMsg;
								mpMsg.setMpMemIndex(mpMsg.getMemIndex());
								if (listMsg.getMpMaxNo() == 1)
								{
									msgList.add(mpMsg);
									incInboundMessageCount();
								}
							}
							else
							{
								if (mpMsg != null)
								{
									if (mpMsg instanceof InboundBinaryMessage)
									{
										InboundBinaryMessage mpMsgBinary = (InboundBinaryMessage) mpMsg;
										InboundBinaryMessage listMsgBinary = (InboundBinaryMessage) listMsg;
										mpMsgBinary.addDataBytes(listMsgBinary.getDataBytes());
									}
									else
									{
										// NEW
										String textToAdd = listMsg.getText();
										if (mpMsg.getEndsWithMultiChar())
										{
											// adjust first char of textToAdd
											Logger.getInstance().logDebug("Adjusting dangling multi-char: " + textToAdd.charAt(0) + " --> " + PduUtils.getMultiCharFor(textToAdd.charAt(0)), null, getGatewayId());
											textToAdd = PduUtils.getMultiCharFor(textToAdd.charAt(0)) + textToAdd.substring(1);
										}
										mpMsg.setEndsWithMultiChar(listMsg.getEndsWithMultiChar());
										try
										{
											mpMsg.addText(textToAdd);
										}
										catch (UnsupportedEncodingException e)
										{
											// TODO: What to do with this?
										}
									}
									mpMsg.setMpSeqNo(listMsg.getMpSeqNo());
									mpMsg.setMpMemIndex(listMsg.getMemIndex());
									if (listMsg.getMpSeqNo() == listMsg.getMpMaxNo())
									{
										mpMsg.setMemIndex(-1);
										msgList.add(mpMsg);
										incInboundMessageCount();
										mpMsg = null;
									}
								}
							}
							break;
						}
					}
				tmpList.clear();
				tmpList = null;
			}
			if (found)
			{
				this.mpMsgList.remove(k);
				k--;
			}
		}
		// Check the remaining parts for "orphaned" status
		for (List<InboundMessage> remainingList : this.mpMsgList)
		{
			for (InboundMessage msg : remainingList)
			{
				if (getAgeInHours(msg.getDate()) > Service.getInstance().getSettings().HOURS_TO_ORPHAN) if (Service.getInstance().getOrphanedMessageNotification() != null) if (Service.getInstance().getOrphanedMessageNotification().process(Service.getInstance().getGateway(msg.getGatewayId()), msg) == true) deleteMessage(msg);
			}
		}
	}

	private long getLastKeepLinkOpen()
	{
		return this.lastKeepLinkOpen;
	}

	private void setLastKeepLinkOpen(long lastKeepLinkOpen)
	{
		this.lastKeepLinkOpen = lastKeepLinkOpen;
	}

	/**
	 * Sets the SIM PIN.
	 * 
	 * @param mySimPin
	 *            The SIM PIN.
	 */
	public void setSimPin(String mySimPin)
	{
		this.simPin = mySimPin;
	}

	/**
	 * Sets the SIM PIN 2.
	 * 
	 * @param mySimPin2
	 *            The SIM PIN 2.
	 */
	public void setSimPin2(String mySimPin2)
	{
		this.simPin2 = mySimPin2;
	}

	/**
	 * Returns the SIM PIN.
	 * 
	 * @return The SIM PIN.
	 */
	public String getSimPin()
	{
		return this.simPin;
	}

	/**
	 * Returns the SIM PIN 2.
	 * 
	 * @return The SIM PIN 2.
	 */
	public String getSimPin2()
	{
		return this.simPin2;
	}

	public AModemDriver getModemDriver()
	{
		return this.driver;
	}

	public AATHandler getATHandler()
	{
		return this.atHandler;
	}

	/**
	 * Returns the Manufacturer string of the modem or phone.
	 * 
	 * @return The Manufacturer string.
	 * @throws TimeoutException
	 *             The gateway did not respond in a timely manner.
	 * @throws GatewayException
	 *             A Gateway error occurred.
	 * @throws IOException
	 *             An IO error occurred.
	 * @throws InterruptedException
	 *             The call was interrupted.
	 */
	public String getManufacturer() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		String response;
		synchronized (getDriver().getSYNCCommander())
		{
			response = getATHandler().getManufacturer();
			if (response.indexOf("ERROR") >= 0) return "N/A";
			response = response.replaceAll("\\s+OK\\s+", "");
			return response;
		}
	}

	/**
	 * Returns the Model string.
	 * 
	 * @return The Model string.
	 * @throws TimeoutException
	 *             The gateway did not respond in a timely manner.
	 * @throws GatewayException
	 *             A Gateway error occurred.
	 * @throws IOException
	 *             An IO error occurred.
	 * @throws InterruptedException
	 *             The call was interrupted.
	 */
	public String getModel() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		String response;
		synchronized (getDriver().getSYNCCommander())
		{
			response = getATHandler().getModel();
			if (response.indexOf("ERROR") >= 0) return "N/A";
			response = response.replaceAll("\\s+OK\\s+", "");
			return response;
		}
	}

	/**
	 * Returns the Serial Number of the modem.
	 * 
	 * @return The Serial Number.
	 * @throws TimeoutException
	 *             The gateway did not respond in a timely manner.
	 * @throws GatewayException
	 *             A Gateway error occurred.
	 * @throws IOException
	 *             An IO error occurred.
	 * @throws InterruptedException
	 *             The call was interrupted.
	 */
	public String getSerialNo() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		String response;
		synchronized (getDriver().getSYNCCommander())
		{
			response = getATHandler().getSerialNo();
			if (response.indexOf("ERROR") >= 0) return "N/A";
			response = response.replaceAll("\\s+OK\\s+", "");
			return response;
		}
	}

	/**
	 * Returns the IMSI (International Mobile Subscriber Identity) number.
	 * <p>
	 * This number is stored in the SIM. Since this number may be used for
	 * several illegal activities, the method is remarked. If you wish to see
	 * your IMSI, just uncomment the method.
	 * 
	 * @return The IMSI.
	 * @throws TimeoutException
	 *             The gateway did not respond in a timely manner.
	 * @throws GatewayException
	 *             A Gateway error occurred.
	 * @throws IOException
	 *             An IO error occurred.
	 * @throws InterruptedException
	 *             The call was interrupted.
	 */
	public String getImsi() throws TimeoutException, GatewayException, IOException, InterruptedException
	{
		if (Service.getInstance().getSettings().MASK_IMSI) return "** MASKED **";
		synchronized (getDriver().getSYNCCommander())
		{
			String response;
			response = getATHandler().getImsi();
			if (response.indexOf("ERROR") >= 0) return "N/A";
