1/*
2 * Copyright (C) 2007-2008 Esmertec AG.
3 * Copyright (C) 2007-2008 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mms.transaction;
19
20import static android.provider.Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION;
21import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
22import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
23import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
24import android.content.BroadcastReceiver;
25import android.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.Intent;
29import android.database.Cursor;
30import android.database.DatabaseUtils;
31import android.database.sqlite.SqliteWrapper;
32import android.net.Uri;
33import android.os.AsyncTask;
34import android.os.PowerManager;
35import android.provider.Telephony.Mms;
36import android.provider.Telephony.Mms.Inbox;
37import android.util.Log;
38
39import com.android.mms.MmsConfig;
40import com.android.mms.ui.MessagingPreferenceActivity;
41import com.google.android.mms.ContentType;
42import com.google.android.mms.MmsException;
43import com.google.android.mms.pdu.DeliveryInd;
44import com.google.android.mms.pdu.GenericPdu;
45import com.google.android.mms.pdu.NotificationInd;
46import com.google.android.mms.pdu.PduHeaders;
47import com.google.android.mms.pdu.PduParser;
48import com.google.android.mms.pdu.PduPersister;
49import com.google.android.mms.pdu.ReadOrigInd;
50
51/**
52 * Receives Intent.WAP_PUSH_RECEIVED_ACTION intents and starts the
53 * TransactionService by passing the push-data to it.
54 */
55public class PushReceiver extends BroadcastReceiver {
56    private static final String TAG = "PushReceiver";
57    private static final boolean DEBUG = false;
58    private static final boolean LOCAL_LOGV = false;
59
60    private class ReceivePushTask extends AsyncTask<Intent,Void,Void> {
61        private Context mContext;
62        public ReceivePushTask(Context context) {
63            mContext = context;
64        }
65
66        @Override
67        protected Void doInBackground(Intent... intents) {
68            Intent intent = intents[0];
69
70            // Get raw PDU push-data from the message and parse it
71            byte[] pushData = intent.getByteArrayExtra("data");
72            PduParser parser = new PduParser(pushData);
73            GenericPdu pdu = parser.parse();
74
75            if (null == pdu) {
76                Log.e(TAG, "Invalid PUSH data");
77                return null;
78            }
79
80            PduPersister p = PduPersister.getPduPersister(mContext);
81            ContentResolver cr = mContext.getContentResolver();
82            int type = pdu.getMessageType();
83            long threadId = -1;
84
85            try {
86                switch (type) {
87                    case MESSAGE_TYPE_DELIVERY_IND:
88                    case MESSAGE_TYPE_READ_ORIG_IND: {
89                        threadId = findThreadId(mContext, pdu, type);
90                        if (threadId == -1) {
91                            // The associated SendReq isn't found, therefore skip
92                            // processing this PDU.
93                            break;
94                        }
95
96                        Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true,
97                                MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null);
98                        // Update thread ID for ReadOrigInd & DeliveryInd.
99                        ContentValues values = new ContentValues(1);
100                        values.put(Mms.THREAD_ID, threadId);
101                        SqliteWrapper.update(mContext, cr, uri, values, null, null);
102                        break;
103                    }
104                    case MESSAGE_TYPE_NOTIFICATION_IND: {
105                        NotificationInd nInd = (NotificationInd) pdu;
106
107                        if (MmsConfig.getTransIdEnabled()) {
108                            byte [] contentLocation = nInd.getContentLocation();
109                            if ('=' == contentLocation[contentLocation.length - 1]) {
110                                byte [] transactionId = nInd.getTransactionId();
111                                byte [] contentLocationWithId = new byte [contentLocation.length
112                                                                          + transactionId.length];
113                                System.arraycopy(contentLocation, 0, contentLocationWithId,
114                                        0, contentLocation.length);
115                                System.arraycopy(transactionId, 0, contentLocationWithId,
116                                        contentLocation.length, transactionId.length);
117                                nInd.setContentLocation(contentLocationWithId);
118                            }
119                        }
120
121                        if (!isDuplicateNotification(mContext, nInd)) {
122                            // Save the pdu. If we can start downloading the real pdu immediately,
123                            // don't allow persist() to create a thread for the notificationInd
124                            // because it causes UI jank.
125                            Uri uri = p.persist(pdu, Inbox.CONTENT_URI,
126                                    !NotificationTransaction.allowAutoDownload(),
127                                    MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext),
128                                    null);
129
130                            // Start service to finish the notification transaction.
131                            Intent svc = new Intent(mContext, TransactionService.class);
132                            svc.putExtra(TransactionBundle.URI, uri.toString());
133                            svc.putExtra(TransactionBundle.TRANSACTION_TYPE,
134                                    Transaction.NOTIFICATION_TRANSACTION);
135                            mContext.startService(svc);
136                        } else if (LOCAL_LOGV) {
137                            Log.v(TAG, "Skip downloading duplicate message: "
138                                    + new String(nInd.getContentLocation()));
139                        }
140                        break;
141                    }
142                    default:
143                        Log.e(TAG, "Received unrecognized PDU.");
144                }
145            } catch (MmsException e) {
146                Log.e(TAG, "Failed to save the data from PUSH: type=" + type, e);
147            } catch (RuntimeException e) {
148                Log.e(TAG, "Unexpected RuntimeException.", e);
149            }
150
151            if (LOCAL_LOGV) {
152                Log.v(TAG, "PUSH Intent processed.");
153            }
154
155            return null;
156        }
157    }
158
159    @Override
160    public void onReceive(Context context, Intent intent) {
161        if (intent.getAction().equals(WAP_PUSH_DELIVER_ACTION)
162                && ContentType.MMS_MESSAGE.equals(intent.getType())) {
163            if (LOCAL_LOGV) {
164                Log.v(TAG, "Received PUSH Intent: " + intent);
165            }
166
167            // Hold a wake lock for 5 seconds, enough to give any
168            // services we start time to take their own wake locks.
169            PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
170            PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
171                                            "MMS PushReceiver");
172            wl.acquire(5000);
173            new ReceivePushTask(context).execute(intent);
174        }
175    }
176
177    private static long findThreadId(Context context, GenericPdu pdu, int type) {
178        String messageId;
179
180        if (type == MESSAGE_TYPE_DELIVERY_IND) {
181            messageId = new String(((DeliveryInd) pdu).getMessageId());
182        } else {
183            messageId = new String(((ReadOrigInd) pdu).getMessageId());
184        }
185
186        StringBuilder sb = new StringBuilder('(');
187        sb.append(Mms.MESSAGE_ID);
188        sb.append('=');
189        sb.append(DatabaseUtils.sqlEscapeString(messageId));
190        sb.append(" AND ");
191        sb.append(Mms.MESSAGE_TYPE);
192        sb.append('=');
193        sb.append(PduHeaders.MESSAGE_TYPE_SEND_REQ);
194        // TODO ContentResolver.query() appends closing ')' to the selection argument
195        // sb.append(')');
196
197        Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
198                            Mms.CONTENT_URI, new String[] { Mms.THREAD_ID },
199                            sb.toString(), null, null);
200        if (cursor != null) {
201            try {
202                if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
203                    return cursor.getLong(0);
204                }
205            } finally {
206                cursor.close();
207            }
208        }
209
210        return -1;
211    }
212
213    private static boolean isDuplicateNotification(
214            Context context, NotificationInd nInd) {
215        byte[] rawLocation = nInd.getContentLocation();
216        if (rawLocation != null) {
217            String location = new String(rawLocation);
218            String selection = Mms.CONTENT_LOCATION + " = ?";
219            String[] selectionArgs = new String[] { location };
220            Cursor cursor = SqliteWrapper.query(
221                    context, context.getContentResolver(),
222                    Mms.CONTENT_URI, new String[] { Mms._ID },
223                    selection, selectionArgs, null);
224            if (cursor != null) {
225                try {
226                    if (cursor.getCount() > 0) {
227                        // We already received the same notification before.
228                        return true;
229                    }
230                } finally {
231                    cursor.close();
232                }
233            }
234        }
235        return false;
236    }
237}
238