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