1/*
2 * Copyright (C) 2014 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.mms.service;
18
19import android.app.Activity;
20import android.app.PendingIntent;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.net.Uri;
25import android.os.Binder;
26import android.os.Bundle;
27import android.os.RemoteException;
28import android.provider.Telephony;
29import android.service.carrier.CarrierMessagingService;
30import android.service.carrier.ICarrierMessagingService;
31import android.telephony.CarrierMessagingServiceManager;
32import android.telephony.SmsManager;
33import android.text.TextUtils;
34
35import com.android.internal.telephony.SmsApplication;
36import com.android.mms.service.exception.MmsHttpException;
37import com.google.android.mms.MmsException;
38import com.google.android.mms.pdu.GenericPdu;
39import com.google.android.mms.pdu.PduHeaders;
40import com.google.android.mms.pdu.PduParser;
41import com.google.android.mms.pdu.PduPersister;
42import com.google.android.mms.pdu.SendConf;
43import com.google.android.mms.pdu.SendReq;
44import com.google.android.mms.util.SqliteWrapper;
45
46/**
47 * Request to send an MMS
48 */
49public class SendRequest extends MmsRequest {
50    private final Uri mPduUri;
51    private byte[] mPduData;
52    private final String mLocationUrl;
53    private final PendingIntent mSentIntent;
54
55    public SendRequest(RequestManager manager, int subId, Uri contentUri, String locationUrl,
56            PendingIntent sentIntent, String creator, Bundle configOverrides, Context context) {
57        super(manager, subId, creator, configOverrides, context);
58        mPduUri = contentUri;
59        mPduData = null;
60        mLocationUrl = locationUrl;
61        mSentIntent = sentIntent;
62    }
63
64    @Override
65    protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn)
66            throws MmsHttpException {
67        final String requestId = this.toString();
68        final MmsHttpClient mmsHttpClient = netMgr.getOrCreateHttpClient();
69        if (mmsHttpClient == null) {
70            LogUtil.e(requestId, "MMS network is not ready!");
71            throw new MmsHttpException(0/*statusCode*/, "MMS network is not ready");
72        }
73        return mmsHttpClient.execute(
74                mLocationUrl != null ? mLocationUrl : apn.getMmscUrl(),
75                mPduData,
76                MmsHttpClient.METHOD_POST,
77                apn.isProxySet(),
78                apn.getProxyAddress(),
79                apn.getProxyPort(),
80                mMmsConfig,
81                mSubId,
82                requestId);
83    }
84
85    @Override
86    protected PendingIntent getPendingIntent() {
87        return mSentIntent;
88    }
89
90    @Override
91    protected int getQueueType() {
92        return MmsService.QUEUE_INDEX_SEND;
93    }
94
95    @Override
96    protected Uri persistIfRequired(Context context, int result, byte[] response) {
97        final String requestId = this.toString();
98        if (!SmsApplication.shouldWriteMessageForPackage(mCreator, context)) {
99            // Not required to persist
100            return null;
101        }
102        LogUtil.d(requestId, "persistIfRequired");
103        if (mPduData == null) {
104            LogUtil.e(requestId, "persistIfRequired: empty PDU");
105            return null;
106        }
107        final long identity = Binder.clearCallingIdentity();
108        try {
109            final boolean supportContentDisposition =
110                    mMmsConfig.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION);
111            // Persist the request PDU first
112            GenericPdu pdu = (new PduParser(mPduData, supportContentDisposition)).parse();
113            if (pdu == null) {
114                LogUtil.e(requestId, "persistIfRequired: can't parse input PDU");
115                return null;
116            }
117            if (!(pdu instanceof SendReq)) {
118                LogUtil.d(requestId, "persistIfRequired: not SendReq");
119                return null;
120            }
121            final PduPersister persister = PduPersister.getPduPersister(context);
122            final Uri messageUri = persister.persist(
123                    pdu,
124                    Telephony.Mms.Sent.CONTENT_URI,
125                    true/*createThreadId*/,
126                    true/*groupMmsEnabled*/,
127                    null/*preOpenedFiles*/);
128            if (messageUri == null) {
129                LogUtil.e(requestId, "persistIfRequired: can not persist message");
130                return null;
131            }
132            // Update the additional columns based on the send result
133            final ContentValues values = new ContentValues();
134            SendConf sendConf = null;
135            if (response != null && response.length > 0) {
136                pdu = (new PduParser(response, supportContentDisposition)).parse();
137                if (pdu != null && pdu instanceof SendConf) {
138                    sendConf = (SendConf) pdu;
139                }
140            }
141            if (result != Activity.RESULT_OK
142                    || sendConf == null
143                    || sendConf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) {
144                // Since we can't persist a message directly into FAILED box,
145                // we have to update the column after we persist it into SENT box.
146                // The gap between the state change is tiny so I would not expect
147                // it to cause any serious problem
148                // TODO: we should add a "failed" URI for this in MmsProvider?
149                values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_FAILED);
150            }
151            if (sendConf != null) {
152                values.put(Telephony.Mms.RESPONSE_STATUS, sendConf.getResponseStatus());
153                values.put(Telephony.Mms.MESSAGE_ID,
154                        PduPersister.toIsoString(sendConf.getMessageId()));
155            }
156            values.put(Telephony.Mms.DATE, System.currentTimeMillis() / 1000L);
157            values.put(Telephony.Mms.READ, 1);
158            values.put(Telephony.Mms.SEEN, 1);
159            if (!TextUtils.isEmpty(mCreator)) {
160                values.put(Telephony.Mms.CREATOR, mCreator);
161            }
162            values.put(Telephony.Mms.SUBSCRIPTION_ID, mSubId);
163            if (SqliteWrapper.update(context, context.getContentResolver(), messageUri, values,
164                    null/*where*/, null/*selectionArg*/) != 1) {
165                LogUtil.e(requestId, "persistIfRequired: failed to update message");
166            }
167            return messageUri;
168        } catch (MmsException e) {
169            LogUtil.e(requestId, "persistIfRequired: can not persist message", e);
170        } catch (RuntimeException e) {
171            LogUtil.e(requestId, "persistIfRequired: unexpected parsing failure", e);
172        } finally {
173            Binder.restoreCallingIdentity(identity);
174        }
175        return null;
176    }
177
178    /**
179     * Read the pdu from the file descriptor and cache pdu bytes in request
180     * @return true if pdu read successfully
181     */
182    private boolean readPduFromContentUri() {
183        if (mPduData != null) {
184            return true;
185        }
186        final int bytesTobeRead = mMmsConfig.getInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE);
187        mPduData = mRequestManager.readPduFromContentUri(mPduUri, bytesTobeRead);
188        return (mPduData != null);
189    }
190
191    /**
192     * Transfer the received response to the caller (for send requests the pdu is small and can
193     *  just include bytes as extra in the "returned" intent).
194     *
195     * @param fillIn the intent that will be returned to the caller
196     * @param response the pdu to transfer
197     */
198    @Override
199    protected boolean transferResponse(Intent fillIn, byte[] response) {
200        // SendConf pdus are always small and can be included in the intent
201        if (response != null) {
202            fillIn.putExtra(SmsManager.EXTRA_MMS_DATA, response);
203        }
204        return true;
205    }
206
207    /**
208     * Read the data from the file descriptor if not yet done
209     * @return whether data successfully read
210     */
211    @Override
212    protected boolean prepareForHttpRequest() {
213        return readPduFromContentUri();
214    }
215
216    /**
217     * Try sending via the carrier app
218     *
219     * @param context the context
220     * @param carrierMessagingServicePackage the carrier messaging service sending the MMS
221     */
222    public void trySendingByCarrierApp(Context context, String carrierMessagingServicePackage) {
223        final CarrierSendManager carrierSendManger = new CarrierSendManager();
224        final CarrierSendCompleteCallback sendCallback = new CarrierSendCompleteCallback(
225                context, carrierSendManger);
226        carrierSendManger.sendMms(context, carrierMessagingServicePackage, sendCallback);
227    }
228
229    @Override
230    protected void revokeUriPermission(Context context) {
231        if (mPduUri != null) {
232            context.revokeUriPermission(mPduUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
233        }
234    }
235
236    /**
237     * Sends the MMS through through the carrier app.
238     */
239    private final class CarrierSendManager extends CarrierMessagingServiceManager {
240        // Initialized in sendMms
241        private volatile CarrierSendCompleteCallback mCarrierSendCompleteCallback;
242
243        void sendMms(Context context, String carrierMessagingServicePackage,
244                CarrierSendCompleteCallback carrierSendCompleteCallback) {
245            mCarrierSendCompleteCallback = carrierSendCompleteCallback;
246            if (bindToCarrierMessagingService(context, carrierMessagingServicePackage)) {
247                LogUtil.v("bindService() for carrier messaging service succeeded");
248            } else {
249                LogUtil.e("bindService() for carrier messaging service failed");
250                carrierSendCompleteCallback.onSendMmsComplete(
251                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
252                        null /* no sendConfPdu */);
253            }
254        }
255
256        @Override
257        protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
258            try {
259                Uri locationUri = null;
260                if (mLocationUrl != null) {
261                    locationUri = Uri.parse(mLocationUrl);
262                }
263                carrierMessagingService.sendMms(mPduUri, mSubId, locationUri,
264                        mCarrierSendCompleteCallback);
265            } catch (RemoteException e) {
266                LogUtil.e("Exception sending MMS using the carrier messaging service: " + e, e);
267                mCarrierSendCompleteCallback.onSendMmsComplete(
268                        CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK,
269                        null /* no sendConfPdu */);
270            }
271        }
272    }
273
274    /**
275     * A callback which notifies carrier messaging app send result. Once the result is ready, the
276     * carrier messaging service connection is disposed.
277     */
278    private final class CarrierSendCompleteCallback extends
279            MmsRequest.CarrierMmsActionCallback {
280        private final Context mContext;
281        private final CarrierSendManager mCarrierSendManager;
282
283        public CarrierSendCompleteCallback(Context context, CarrierSendManager carrierSendManager) {
284            mContext = context;
285            mCarrierSendManager = carrierSendManager;
286        }
287
288        @Override
289        public void onSendMmsComplete(int result, byte[] sendConfPdu) {
290            LogUtil.d("Carrier app result for send: " + result);
291            mCarrierSendManager.disposeConnection(mContext);
292
293            if (!maybeFallbackToRegularDelivery(result)) {
294                processResult(mContext, toSmsManagerResult(result), sendConfPdu,
295                        0/* httpStatusCode */);
296            }
297        }
298
299        @Override
300        public void onDownloadMmsComplete(int result) {
301            LogUtil.e("Unexpected onDownloadMmsComplete call with result: " + result);
302        }
303    }
304}
305