Home | History | Annotate | Download | only in telephony
      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 
     17 package com.android.internal.telephony;
     18 
     19 import com.google.android.mms.MmsException;
     20 import com.google.android.mms.pdu.DeliveryInd;
     21 import com.google.android.mms.pdu.GenericPdu;
     22 import com.google.android.mms.pdu.NotificationInd;
     23 import com.google.android.mms.pdu.PduHeaders;
     24 import com.google.android.mms.pdu.PduParser;
     25 import com.google.android.mms.pdu.PduPersister;
     26 import com.google.android.mms.pdu.ReadOrigInd;
     27 
     28 import android.app.Activity;
     29 import android.app.AppOpsManager;
     30 import android.content.BroadcastReceiver;
     31 import android.content.ComponentName;
     32 import android.content.ContentValues;
     33 import android.content.Context;
     34 import android.content.Intent;
     35 import android.content.ServiceConnection;
     36 import android.database.Cursor;
     37 import android.database.DatabaseUtils;
     38 import android.database.sqlite.SQLiteException;
     39 import android.database.sqlite.SqliteWrapper;
     40 import android.net.Uri;
     41 import android.os.Bundle;
     42 import android.os.IBinder;
     43 import android.os.RemoteException;
     44 import android.os.UserHandle;
     45 import android.provider.Telephony;
     46 import android.provider.Telephony.Sms.Intents;
     47 import android.telephony.SmsManager;
     48 import android.telephony.SubscriptionManager;
     49 import android.telephony.Rlog;
     50 import android.util.Log;
     51 
     52 import com.android.internal.telephony.uicc.IccUtils;
     53 
     54 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
     55 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
     56 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
     57 
     58 /**
     59  * WAP push handler class.
     60  *
     61  * @hide
     62  */
     63 public 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