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