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