WapPushOverSms.java revision f1aeeb51f2c62420012122e0ccc75b3940c570e4
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.telephony;
18
19import com.google.android.mms.MmsException;
20import com.google.android.mms.pdu.DeliveryInd;
21import com.google.android.mms.pdu.GenericPdu;
22import com.google.android.mms.pdu.NotificationInd;
23import com.google.android.mms.pdu.PduHeaders;
24import com.google.android.mms.pdu.PduParser;
25import com.google.android.mms.pdu.PduPersister;
26import com.google.android.mms.pdu.ReadOrigInd;
27
28import android.app.Activity;
29import android.app.AppOpsManager;
30import android.content.BroadcastReceiver;
31import android.content.ComponentName;
32import android.content.ContentValues;
33import android.content.Context;
34import android.content.Intent;
35import android.content.ServiceConnection;
36import android.database.Cursor;
37import android.database.DatabaseUtils;
38import android.database.sqlite.SQLiteException;
39import android.database.sqlite.SqliteWrapper;
40import android.net.Uri;
41import android.os.IBinder;
42import android.os.RemoteException;
43import android.provider.Telephony;
44import android.provider.Telephony.Sms.Intents;
45import android.telephony.MessagingConfigurationManager;
46import android.telephony.SmsManager;
47import android.telephony.SubscriptionManager;
48import android.telephony.Rlog;
49import android.util.Log;
50
51import com.android.internal.telephony.uicc.IccUtils;
52
53import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
54import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
55import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
56
57/**
58 * WAP push handler class.
59 *
60 * @hide
61 */
62public class WapPushOverSms implements ServiceConnection {
63    private static final String TAG = "WAP PUSH";
64    private static final boolean DBG = true;
65
66    private final Context mContext;
67
68    /** Assigned from ServiceConnection callback on main threaad. */
69    private volatile IWapPushManager mWapPushManager;
70
71    @Override
72    public void onServiceConnected(ComponentName name, IBinder service) {
73        mWapPushManager = IWapPushManager.Stub.asInterface(service);
74        if (DBG) Rlog.v(TAG, "wappush manager connected to " + hashCode());
75    }
76
77    @Override
78    public void onServiceDisconnected(ComponentName name) {
79        mWapPushManager = null;
80        if (DBG) Rlog.v(TAG, "wappush manager disconnected.");
81    }
82
83    public WapPushOverSms(Context context) {
84        mContext = context;
85        Intent intent = new Intent(IWapPushManager.class.getName());
86        ComponentName comp = intent.resolveSystemService(context.getPackageManager(), 0);
87        intent.setComponent(comp);
88        if (comp == null || !context.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
89            Rlog.e(TAG, "bindService() for wappush manager failed");
90        } else {
91            if (DBG) Rlog.v(TAG, "bindService() for wappush manager succeeded");
92        }
93    }
94
95    void dispose() {
96        if (mWapPushManager != null) {
97            if (DBG) Rlog.v(TAG, "dispose: unbind wappush manager");
98            mContext.unbindService(this);
99        } else {
100            Rlog.e(TAG, "dispose: not bound to a wappush manager");
101        }
102    }
103
104    /**
105     * Dispatches inbound messages that are in the WAP PDU format. See
106     * wap-230-wsp-20010705-a section 8 for details on the WAP PDU format.
107     *
108     * @param pdu The WAP PDU, made up of one or more SMS PDUs
109     * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
110     *         {@link Activity#RESULT_OK} if the message has been broadcast
111     *         to applications
112     */
113    public int dispatchWapPdu(byte[] pdu, BroadcastReceiver receiver, InboundSmsHandler handler) {
114
115        if (DBG) Rlog.d(TAG, "Rx: " + IccUtils.bytesToHexString(pdu));
116
117        try {
118            int index = 0;
119            int transactionId = pdu[index++] & 0xFF;
120            int pduType = pdu[index++] & 0xFF;
121
122            // Should we "abort" if no subId for now just no supplying extra param below
123            int phoneId = handler.getPhone().getPhoneId();
124
125            if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) &&
126                    (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
127                index = mContext.getResources().getInteger(
128                        com.android.internal.R.integer.config_valid_wappush_index);
129                if (index != -1) {
130                    transactionId = pdu[index++] & 0xff;
131                    pduType = pdu[index++] & 0xff;
132                    if (DBG)
133                        Rlog.d(TAG, "index = " + index + " PDU Type = " + pduType +
134                                " transactionID = " + transactionId);
135
136                    // recheck wap push pduType
137                    if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH)
138                            && (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
139                        if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
140                        return Intents.RESULT_SMS_HANDLED;
141                    }
142                } else {
143                    if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
144                    return Intents.RESULT_SMS_HANDLED;
145                }
146            }
147
148            WspTypeDecoder pduDecoder = new WspTypeDecoder(pdu);
149
150            /**
151             * Parse HeaderLen(unsigned integer).
152             * From wap-230-wsp-20010705-a section 8.1.2
153             * The maximum size of a uintvar is 32 bits.
154             * So it will be encoded in no more than 5 octets.
155             */
156            if (pduDecoder.decodeUintvarInteger(index) == false) {
157                if (DBG) Rlog.w(TAG, "Received PDU. Header Length error.");
158                return Intents.RESULT_SMS_GENERIC_ERROR;
159            }
160            int headerLength = (int) pduDecoder.getValue32();
161            index += pduDecoder.getDecodedDataLength();
162
163            int headerStartIndex = index;
164
165            /**
166             * Parse Content-Type.
167             * From wap-230-wsp-20010705-a section 8.4.2.24
168             *
169             * Content-type-value = Constrained-media | Content-general-form
170             * Content-general-form = Value-length Media-type
171             * Media-type = (Well-known-media | Extension-Media) *(Parameter)
172             * Value-length = Short-length | (Length-quote Length)
173             * Short-length = <Any octet 0-30>   (octet <= WAP_PDU_SHORT_LENGTH_MAX)
174             * Length-quote = <Octet 31>         (WAP_PDU_LENGTH_QUOTE)
175             * Length = Uintvar-integer
176             */
177            if (pduDecoder.decodeContentType(index) == false) {
178                if (DBG) Rlog.w(TAG, "Received PDU. Header Content-Type error.");
179                return Intents.RESULT_SMS_GENERIC_ERROR;
180            }
181
182            String mimeType = pduDecoder.getValueString();
183            long binaryContentType = pduDecoder.getValue32();
184            index += pduDecoder.getDecodedDataLength();
185
186            byte[] header = new byte[headerLength];
187            System.arraycopy(pdu, headerStartIndex, header, 0, header.length);
188
189            byte[] intentData;
190
191            if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) {
192                intentData = pdu;
193            } else {
194                int dataIndex = headerStartIndex + headerLength;
195                intentData = new byte[pdu.length - dataIndex];
196                System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length);
197            }
198
199            if (SmsManager.getDefault().getAutoPersisting()) {
200                // Store the wap push data in telephony
201                writeInboxMessage(intentData);
202            }
203
204            /**
205             * Seek for application ID field in WSP header.
206             * If application ID is found, WapPushManager substitute the message
207             * processing. Since WapPushManager is optional module, if WapPushManager
208             * is not found, legacy message processing will be continued.
209             */
210            if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) {
211                index = (int) pduDecoder.getValue32();
212                pduDecoder.decodeXWapApplicationId(index);
213                String wapAppId = pduDecoder.getValueString();
214                if (wapAppId == null) {
215                    wapAppId = Integer.toString((int) pduDecoder.getValue32());
216                }
217
218                String contentType = ((mimeType == null) ?
219                        Long.toString(binaryContentType) : mimeType);
220                if (DBG) Rlog.v(TAG, "appid found: " + wapAppId + ":" + contentType);
221
222                try {
223                    boolean processFurther = true;
224                    IWapPushManager wapPushMan = mWapPushManager;
225
226                    if (wapPushMan == null) {
227                        if (DBG) Rlog.w(TAG, "wap push manager not found!");
228                    } else {
229                        Intent intent = new Intent();
230                        intent.putExtra("transactionId", transactionId);
231                        intent.putExtra("pduType", pduType);
232                        intent.putExtra("header", header);
233                        intent.putExtra("data", intentData);
234                        intent.putExtra("contentTypeParameters",
235                                pduDecoder.getContentParameters());
236                        SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
237
238                        int procRet = wapPushMan.processMessage(wapAppId, contentType, intent);
239                        if (DBG) Rlog.v(TAG, "procRet:" + procRet);
240                        if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0
241                                && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) {
242                            processFurther = false;
243                        }
244                    }
245                    if (!processFurther) {
246                        return Intents.RESULT_SMS_HANDLED;
247                    }
248                } catch (RemoteException e) {
249                    if (DBG) Rlog.w(TAG, "remote func failed...");
250                }
251            }
252            if (DBG) Rlog.v(TAG, "fall back to existing handler");
253
254            if (mimeType == null) {
255                if (DBG) Rlog.w(TAG, "Header Content-Type error.");
256                return Intents.RESULT_SMS_GENERIC_ERROR;
257            }
258
259            String permission;
260            int appOp;
261
262            if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) {
263                permission = android.Manifest.permission.RECEIVE_MMS;
264                appOp = AppOpsManager.OP_RECEIVE_MMS;
265            } else {
266                permission = android.Manifest.permission.RECEIVE_WAP_PUSH;
267                appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH;
268            }
269
270            Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
271            intent.setType(mimeType);
272            intent.putExtra("transactionId", transactionId);
273            intent.putExtra("pduType", pduType);
274            intent.putExtra("header", header);
275            intent.putExtra("data", intentData);
276            intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());
277            SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
278
279            // Direct the intent to only the default MMS app. If we can't find a default MMS app
280            // then sent it to all broadcast receivers.
281            ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true);
282            if (componentName != null) {
283                // Deliver MMS message only to this receiver
284                intent.setComponent(componentName);
285                if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() +
286                        " " + componentName.getClassName());
287            }
288
289            handler.dispatchIntent(intent, permission, appOp, receiver);
290            return Activity.RESULT_OK;
291        } catch (ArrayIndexOutOfBoundsException aie) {
292            // 0-byte WAP PDU or other unexpected WAP PDU contents can easily throw this;
293            // log exception string without stack trace and return false.
294            Rlog.e(TAG, "ignoring dispatchWapPdu() array index exception: " + aie);
295            return Intents.RESULT_SMS_GENERIC_ERROR;
296        }
297    }
298
299    private void writeInboxMessage(byte[] pushData) {
300        final GenericPdu pdu = new PduParser(pushData).parse();
301        if (pdu == null) {
302            Rlog.e(TAG, "Invalid PUSH PDU");
303        }
304        final PduPersister persister = PduPersister.getPduPersister(mContext);
305        final int type = pdu.getMessageType();
306        try {
307            switch (type) {
308                case MESSAGE_TYPE_DELIVERY_IND:
309                case MESSAGE_TYPE_READ_ORIG_IND: {
310                    final long threadId = getDeliveryOrReadReportThreadId(mContext, pdu);
311                    if (threadId == -1) {
312                        // The associated SendReq isn't found, therefore skip
313                        // processing this PDU.
314                        Rlog.e(TAG, "Failed to find delivery or read report's thread id");
315                        break;
316                    }
317                    final Uri uri = persister.persist(
318                            pdu,
319                            Telephony.Mms.Inbox.CONTENT_URI,
320                            true/*createThreadId*/,
321                            true/*groupMmsEnabled*/,
322                            null/*preOpenedFiles*/);
323                    if (uri == null) {
324                        Rlog.e(TAG, "Failed to persist delivery or read report");
325                        break;
326                    }
327                    // Update thread ID for ReadOrigInd & DeliveryInd.
328                    final ContentValues values = new ContentValues(1);
329                    values.put(Telephony.Mms.THREAD_ID, threadId);
330                    if (SqliteWrapper.update(
331                            mContext,
332                            mContext.getContentResolver(),
333                            uri,
334                            values,
335                            null/*where*/,
336                            null/*selectionArgs*/) != 1) {
337                        Rlog.e(TAG, "Failed to update delivery or read report thread id");
338                    }
339                    break;
340                }
341                case MESSAGE_TYPE_NOTIFICATION_IND: {
342                    final NotificationInd nInd = (NotificationInd) pdu;
343
344                    if (MessagingConfigurationManager.getDefault().getCarrierConfigBoolean(
345                            MessagingConfigurationManager.CONF_APPEND_TRANSACTION_ID, false)) {
346                        final byte [] contentLocation = nInd.getContentLocation();
347                        if ('=' == contentLocation[contentLocation.length - 1]) {
348                            byte [] transactionId = nInd.getTransactionId();
349                            byte [] contentLocationWithId = new byte [contentLocation.length
350                                    + transactionId.length];
351                            System.arraycopy(contentLocation, 0, contentLocationWithId,
352                                    0, contentLocation.length);
353                            System.arraycopy(transactionId, 0, contentLocationWithId,
354                                    contentLocation.length, transactionId.length);
355                            nInd.setContentLocation(contentLocationWithId);
356                        }
357                    }
358                    if (!isDuplicateNotification(mContext, nInd)) {
359                        final Uri uri = persister.persist(
360                                pdu,
361                                Telephony.Mms.Inbox.CONTENT_URI,
362                                true/*createThreadId*/,
363                                true/*groupMmsEnabled*/,
364                                null/*preOpenedFiles*/);
365                        if (uri == null) {
366                            Rlog.e(TAG, "Failed to save MMS WAP push notification ind");
367                        }
368                    } else {
369                        Rlog.d(TAG, "Skip storing duplicate MMS WAP push notification ind: "
370                                + new String(nInd.getContentLocation()));
371                    }
372                    break;
373                }
374                default:
375                    Log.e(TAG, "Received unrecognized WAP Push PDU.");
376            }
377        } catch (MmsException e) {
378            Log.e(TAG, "Failed to save MMS WAP push data: type=" + type, e);
379        } catch (RuntimeException e) {
380            Log.e(TAG, "Unexpected RuntimeException in persisting MMS WAP push data", e);
381        }
382
383    }
384
385    private static final String THREAD_ID_SELECTION =
386            Telephony.Mms.MESSAGE_ID + "=? AND " + Telephony.Mms.MESSAGE_TYPE + "=?";
387
388    private static long getDeliveryOrReadReportThreadId(Context context, GenericPdu pdu) {
389        String messageId;
390        if (pdu instanceof DeliveryInd) {
391            messageId = new String(((DeliveryInd) pdu).getMessageId());
392        } else if (pdu instanceof ReadOrigInd) {
393            messageId = new String(((ReadOrigInd) pdu).getMessageId());
394        } else {
395            Rlog.e(TAG, "WAP Push data is neither delivery or read report type: "
396                    + pdu.getClass().getCanonicalName());
397            return -1L;
398        }
399        Cursor cursor = null;
400        try {
401            cursor = SqliteWrapper.query(
402                    context,
403                    context.getContentResolver(),
404                    Telephony.Mms.CONTENT_URI,
405                    new String[]{ Telephony.Mms.THREAD_ID },
406                    THREAD_ID_SELECTION,
407                    new String[]{
408                            DatabaseUtils.sqlEscapeString(messageId),
409                            Integer.toString(PduHeaders.MESSAGE_TYPE_SEND_REQ)
410                    },
411                    null/*sortOrder*/);
412            if (cursor != null && cursor.moveToFirst()) {
413                return cursor.getLong(0);
414            }
415        } catch (SQLiteException e) {
416            Rlog.e(TAG, "Failed to query delivery or read report thread id", e);
417        } finally {
418            if (cursor != null) {
419                cursor.close();
420            }
421        }
422        return -1L;
423    }
424
425    private static final String LOCATION_SELECTION =
426            Telephony.Mms.MESSAGE_TYPE + "=? AND " + Telephony.Mms.CONTENT_LOCATION + " =?";
427
428    private static boolean isDuplicateNotification(Context context, NotificationInd nInd) {
429        final byte[] rawLocation = nInd.getContentLocation();
430        if (rawLocation != null) {
431            String location = new String(rawLocation);
432            String[] selectionArgs = new String[] { location };
433            Cursor cursor = null;
434            try {
435                cursor = SqliteWrapper.query(
436                        context,
437                        context.getContentResolver(),
438                        Telephony.Mms.CONTENT_URI,
439                        new String[]{Telephony.Mms._ID},
440                        LOCATION_SELECTION,
441                        new String[]{
442                                Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
443                                new String(rawLocation)
444                        },
445                        null/*sortOrder*/);
446                if (cursor != null && cursor.getCount() > 0) {
447                    // We already received the same notification before.
448                    return true;
449                }
450            } catch (SQLiteException e) {
451                Rlog.e(TAG, "failed to query existing notification ind", e);
452            } finally {
453                if (cursor != null) {
454                    cursor.close();
455                }
456            }
457        }
458        return false;
459    }
460}
461