/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.goanna; import java.util.ArrayList; import java.util.Iterator; import android.util.Log; import android.app.PendingIntent; import android.app.Activity; import android.database.Cursor; import android.content.Intent; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContentResolver; import android.content.ContentValues; import android.content.ContentUris; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.telephony.SmsManager; import android.telephony.SmsMessage; import static android.telephony.SmsMessage.MessageClass; /** * This class is returning unique ids for PendingIntent requestCode attribute. * There are only |Integer.MAX_VALUE - Integer.MIN_VALUE| unique IDs available, * and they wrap around. */ class PendingIntentUID { static private int sUID = Integer.MIN_VALUE; static public int generate() { return sUID++; } } /** * The envelope class contains all information that are needed to keep track of * a sent SMS. */ class Envelope { enum SubParts { SENT_PART, DELIVERED_PART } protected int mId; protected int mMessageId; protected long mMessageTimestamp; /** * Number of sent/delivered remaining parts. * @note The array has much slots as SubParts items. */ protected int[] mRemainingParts; /** * Whether sending/delivering is currently failing. * @note The array has much slots as SubParts items. */ protected boolean[] mFailing; /** * Error type (only for sent). */ protected int mError; public Envelope(int aId, int aParts) { mId = aId; mMessageId = -1; mMessageTimestamp = 0; mError = GoannaSmsManager.kNoError; int size = Envelope.SubParts.values().length; mRemainingParts = new int[size]; mFailing = new boolean[size]; for (int i=0; i mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) { Log.e("GoannaSmsManager", "Delivered more parts than we sent!?"); } } public boolean arePartsRemaining(Envelope.SubParts aType) { return mRemainingParts[aType.ordinal()] != 0; } public void markAsFailed(Envelope.SubParts aType) { mFailing[aType.ordinal()] = true; } public boolean isFailing(Envelope.SubParts aType) { return mFailing[aType.ordinal()]; } public int getMessageId() { return mMessageId; } public void setMessageId(int aMessageId) { mMessageId = aMessageId; } public long getMessageTimestamp() { return mMessageTimestamp; } public void setMessageTimestamp(long aMessageTimestamp) { mMessageTimestamp = aMessageTimestamp; } public int getError() { return mError; } public void setError(int aError) { mError = aError; } } /** * Postman class is a singleton that manages Envelope instances. */ class Postman { public static final int kUnknownEnvelopeId = -1; private static final Postman sInstance = new Postman(); private ArrayList mEnvelopes = new ArrayList(1); private Postman() {} public static Postman getInstance() { return sInstance; } public int createEnvelope(int aParts) { /* * We are going to create the envelope in the first empty slot in the array * list. If there is no empty slot, we create a new one. */ int size = mEnvelopes.size(); for (int i=0; i mCursors = new ArrayList(0); public int add(Cursor aCursor) { int size = mCursors.size(); for (int i=0; i parts = sm.divideMessage(aMessage); envelopeId = Postman.getInstance().createEnvelope(parts.size()); bundle.putInt("envelopeId", envelopeId); sentIntent.putExtras(bundle); deliveredIntent.putExtras(bundle); ArrayList sentPendingIntents = new ArrayList(parts.size()); ArrayList deliveredPendingIntents = new ArrayList(parts.size()); for (int i=0; i Integer.MAX_VALUE) { throw new IdTooHighException(); } return (int)id; } catch (IdTooHighException e) { Log.e("GoannaSmsManager", "The id we received is higher than the higher allowed value."); return -1; } catch (Exception e) { Log.e("GoannaSmsManager", "Something went wrong when trying to write a sent message: " + e); return -1; } } public void getMessage(int aMessageId, int aRequestId) { class GetMessageRunnable implements Runnable { private int mMessageId; private int mRequestId; GetMessageRunnable(int aMessageId, int aRequestId) { mMessageId = aMessageId; mRequestId = aRequestId; } @Override public void run() { Cursor cursor = null; try { ContentResolver cr = GoannaApp.mAppContext.getContentResolver(); Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId); cursor = cr.query(message, kRequiredMessageRows, null, null, null); if (cursor == null || cursor.getCount() == 0) { throw new NotFoundException(); } if (cursor.getCount() != 1) { throw new TooManyResultsException(); } cursor.moveToFirst(); if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) { throw new UnmatchingIdException(); } int type = cursor.getInt(cursor.getColumnIndex("type")); int deliveryStatus; String sender = ""; String receiver = ""; if (type == kSmsTypeInbox) { deliveryStatus = kDeliveryStatusSuccess; sender = cursor.getString(cursor.getColumnIndex("address")); } else if (type == kSmsTypeSentbox) { deliveryStatus = getGoannaDeliveryStatus(cursor.getInt(cursor.getColumnIndex("status"))); receiver = cursor.getString(cursor.getColumnIndex("address")); } else { throw new InvalidTypeException(); } GoannaAppShell.notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")), deliveryStatus, receiver, sender, cursor.getString(cursor.getColumnIndex("body")), cursor.getLong(cursor.getColumnIndex("date")), mRequestId); } catch (NotFoundException e) { Log.i("GoannaSmsManager", "Message id " + mMessageId + " not found"); GoannaAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId); } catch (UnmatchingIdException e) { Log.e("GoannaSmsManager", "Requested message id (" + mMessageId + ") is different from the one we got."); GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId); } catch (TooManyResultsException e) { Log.e("GoannaSmsManager", "Get too many results for id " + mMessageId); GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId); } catch (InvalidTypeException e) { Log.i("GoannaSmsManager", "Message has an invalid type, we ignore it."); GoannaAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId); } catch (Exception e) { Log.e("GoannaSmsManager", "Error while trying to get message: " + e); GoannaAppShell.notifyGetSmsFailed(kUnknownError, mRequestId); } finally { if (cursor != null) { cursor.close(); } } } } if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId))) { Log.e("GoannaSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread"); GoannaAppShell.notifyGetSmsFailed(kUnknownError, aRequestId); } } public void deleteMessage(int aMessageId, int aRequestId) { class DeleteMessageRunnable implements Runnable { private int mMessageId; private int mRequestId; DeleteMessageRunnable(int aMessageId, int aRequestId) { mMessageId = aMessageId; mRequestId = aRequestId; } @Override public void run() { try { ContentResolver cr = GoannaApp.mAppContext.getContentResolver(); Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId); int count = cr.delete(message, null, null); if (count > 1) { throw new TooManyResultsException(); } GoannaAppShell.notifySmsDeleted(count == 1, mRequestId); } catch (TooManyResultsException e) { Log.e("GoannaSmsManager", "Delete more than one message? " + e); GoannaAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId); } catch (Exception e) { Log.e("GoannaSmsManager", "Error while trying to delete a message: " + e); GoannaAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId); } } } if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId))) { Log.e("GoannaSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread"); GoannaAppShell.notifySmsDeleteFailed(kUnknownError, aRequestId); } } public void createMessageList(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId) { class CreateMessageListRunnable implements Runnable { private long mStartDate; private long mEndDate; private String[] mNumbers; private int mNumbersCount; private int mDeliveryState; private boolean mReverse; private int mRequestId; CreateMessageListRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId) { mStartDate = aStartDate; mEndDate = aEndDate; mNumbers = aNumbers; mNumbersCount = aNumbersCount; mDeliveryState = aDeliveryState; mReverse = aReverse; mRequestId = aRequestId; } @Override public void run() { Cursor cursor = null; boolean closeCursor = true; try { // TODO: should use the |selectionArgs| argument in |ContentResolver.query()|. ArrayList restrictions = new ArrayList(); if (mStartDate != 0) { restrictions.add("date >= " + mStartDate); } if (mEndDate != 0) { restrictions.add("date <= " + mEndDate); } if (mNumbersCount > 0) { String numberRestriction = "address IN ('" + mNumbers[0] + "'"; for (int i=1; i= kInternalDeliveryStatusFailed) { return kDeliveryStatusError; } if (aDeliveryStatus >= kInternalDeliveryStatusPending) { return kDeliveryStatusPending; } return kDeliveryStatusSuccess; } private int getGoannaMessageClass(MessageClass aMessageClass) { switch (aMessageClass) { case UNKNOWN: return kMessageClassNormal; case CLASS_0: return kMessageClassClass0; case CLASS_1: return kMessageClassClass1; case CLASS_2: return kMessageClassClass2; case CLASS_3: return kMessageClassClass3; } } class IdTooHighException extends Exception { private static final long serialVersionUID = 395697882128640L; } class InvalidTypeException extends Exception { private static final long serialVersionUID = 23359904803795434L; } class NotFoundException extends Exception { private static final long serialVersionUID = 266226999371957426L; } class TooManyResultsException extends Exception { private static final long serialVersionUID = 48899777673841920L; } class UnexpectedDeliveryStateException extends Exception { private static final long serialVersionUID = 5044567998961920L; } class UnmatchingIdException extends Exception { private static final long serialVersionUID = 1935649715512128L; } }