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