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.Context;
22import android.content.Intent;
23import android.net.Uri;
24import android.os.Bundle;
25import android.service.carrier.CarrierMessagingService;
26import android.service.carrier.ICarrierMessagingCallback;
27import android.telephony.SmsManager;
28import android.telephony.TelephonyManager;
29import android.text.TextUtils;
30
31import com.android.mms.service.exception.ApnException;
32import com.android.mms.service.exception.MmsHttpException;
33import com.android.mms.service.exception.MmsNetworkException;
34
35/**
36 * Base class for MMS requests. This has the common logic of sending/downloading MMS.
37 */
38public abstract class MmsRequest {
39    private static final int RETRY_TIMES = 3;
40
41    /**
42     * Interface for certain functionalities from MmsService
43     */
44    public static interface RequestManager {
45        /**
46         * Enqueue an MMS request
47         *
48         * @param request the request to enqueue
49         */
50        public void addSimRequest(MmsRequest request);
51
52        /*
53         * @return Whether to auto persist received MMS
54         */
55        public boolean getAutoPersistingPref();
56
57        /**
58         * Read pdu (up to maxSize bytes) from supplied content uri
59         * @param contentUri content uri from which to read
60         * @param maxSize maximum number of bytes to read
61         * @return read pdu (else null in case of error or too big)
62         */
63        public byte[] readPduFromContentUri(final Uri contentUri, final int maxSize);
64
65        /**
66         * Write pdu to supplied content uri
67         * @param contentUri content uri to which bytes should be written
68         * @param pdu pdu bytes to write
69         * @return true in case of success (else false)
70         */
71        public boolean writePduToContentUri(final Uri contentUri, final byte[] pdu);
72    }
73
74    // The reference to the pending requests manager (i.e. the MmsService)
75    protected RequestManager mRequestManager;
76    // The SIM id
77    protected int mSubId;
78    // The creator app
79    protected String mCreator;
80    // MMS config
81    protected Bundle mMmsConfig;
82    // MMS config overrides that will be applied to mMmsConfig when we eventually load it.
83    protected Bundle mMmsConfigOverrides;
84    // Context used to get TelephonyManager.
85    protected Context mContext;
86
87    public MmsRequest(RequestManager requestManager, int subId, String creator,
88            Bundle configOverrides, Context context) {
89        mRequestManager = requestManager;
90        mSubId = subId;
91        mCreator = creator;
92        mMmsConfigOverrides = configOverrides;
93        mMmsConfig = null;
94        mContext = context;
95    }
96
97    public int getSubId() {
98        return mSubId;
99    }
100
101    private boolean ensureMmsConfigLoaded() {
102        if (mMmsConfig == null) {
103            // Not yet retrieved from mms config manager. Try getting it.
104            final Bundle config = MmsConfigManager.getInstance().getMmsConfigBySubId(mSubId);
105            if (config != null) {
106                mMmsConfig = config;
107                // TODO: Make MmsConfigManager authoritative for user agent and don't consult
108                // TelephonyManager.
109                final TelephonyManager telephonyManager =
110                        (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
111                final String userAgent = telephonyManager.getMmsUserAgent();
112                if (!TextUtils.isEmpty(userAgent)) {
113                    config.putString(SmsManager.MMS_CONFIG_USER_AGENT, userAgent);
114                }
115                final String userAgentProfileUrl = telephonyManager.getMmsUAProfUrl();
116                if (!TextUtils.isEmpty(userAgentProfileUrl)) {
117                    config.putString(SmsManager.MMS_CONFIG_UA_PROF_URL, userAgentProfileUrl);
118                }
119                // Apply overrides
120                if (mMmsConfigOverrides != null) {
121                    mMmsConfig.putAll(mMmsConfigOverrides);
122                }
123            }
124        }
125        return mMmsConfig != null;
126    }
127
128    /**
129     * Execute the request
130     *
131     * @param context The context
132     * @param networkManager The network manager to use
133     */
134    public void execute(Context context, MmsNetworkManager networkManager) {
135        final String requestId = this.toString();
136        LogUtil.i(requestId, "Executing...");
137        int result = SmsManager.MMS_ERROR_UNSPECIFIED;
138        int httpStatusCode = 0;
139        byte[] response = null;
140        // TODO: add mms data channel check back to fast fail if no way to send mms,
141        // when telephony provides such API.
142        if (!ensureMmsConfigLoaded()) { // Check mms config
143            LogUtil.e(requestId, "mms config is not loaded yet");
144            result = SmsManager.MMS_ERROR_CONFIGURATION_ERROR;
145        } else if (!prepareForHttpRequest()) { // Prepare request, like reading pdu data from user
146            LogUtil.e(requestId, "Failed to prepare for request");
147            result = SmsManager.MMS_ERROR_IO_ERROR;
148        } else { // Execute
149            long retryDelaySecs = 2;
150            // Try multiple times of MMS HTTP request, depending on the error.
151            for (int i = 0; i < RETRY_TIMES; i++) {
152                try {
153                    networkManager.acquireNetwork(requestId);
154                    final String apnName = networkManager.getApnName();
155                    LogUtil.d(requestId, "APN name is " + apnName);
156                    try {
157                        ApnSettings apn = null;
158                        try {
159                            apn = ApnSettings.load(context, apnName, mSubId, requestId);
160                        } catch (ApnException e) {
161                            // If no APN could be found, fall back to trying without the APN name
162                            if (apnName == null) {
163                                // If the APN name was already null then don't need to retry
164                                throw (e);
165                            }
166                            LogUtil.i(requestId, "No match with APN name: "
167                                    + apnName + ", try with no name");
168                            apn = ApnSettings.load(context, null, mSubId, requestId);
169                        }
170                        LogUtil.i(requestId, "Using " + apn.toString());
171                        response = doHttp(context, networkManager, apn);
172                        result = Activity.RESULT_OK;
173                        // Success
174                        break;
175                    } finally {
176                        networkManager.releaseNetwork(requestId, this instanceof DownloadRequest);
177                    }
178                } catch (ApnException e) {
179                    LogUtil.e(requestId, "APN failure", e);
180                    result = SmsManager.MMS_ERROR_INVALID_APN;
181                    break;
182                } catch (MmsNetworkException e) {
183                    LogUtil.e(requestId, "MMS network acquiring failure", e);
184                    result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS;
185                    break;
186                } catch (MmsHttpException e) {
187                    LogUtil.e(requestId, "HTTP or network I/O failure", e);
188                    result = SmsManager.MMS_ERROR_HTTP_FAILURE;
189                    httpStatusCode = e.getStatusCode();
190                    // Retry
191                } catch (Exception e) {
192                    LogUtil.e(requestId, "Unexpected failure", e);
193                    result = SmsManager.MMS_ERROR_UNSPECIFIED;
194                    break;
195                }
196                try {
197                    Thread.sleep(retryDelaySecs * 1000, 0/*nano*/);
198                } catch (InterruptedException e) {}
199                retryDelaySecs <<= 1;
200            }
201        }
202        processResult(context, result, response, httpStatusCode);
203    }
204
205    /**
206     * Process the result of the completed request, including updating the message status
207     * in database and sending back the result via pending intents.
208     *  @param context The context
209     * @param result The result code of execution
210     * @param response The response body
211     * @param httpStatusCode The optional http status code in case of http failure
212     */
213    public void processResult(Context context, int result, byte[] response, int httpStatusCode) {
214        final Uri messageUri = persistIfRequired(context, result, response);
215
216        // Return MMS HTTP request result via PendingIntent
217        final PendingIntent pendingIntent = getPendingIntent();
218        if (pendingIntent != null) {
219            boolean succeeded = true;
220            // Extra information to send back with the pending intent
221            Intent fillIn = new Intent();
222            if (response != null) {
223                succeeded = transferResponse(fillIn, response);
224            }
225            if (messageUri != null) {
226                fillIn.putExtra("uri", messageUri.toString());
227            }
228            if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) {
229                fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode);
230            }
231            try {
232                if (!succeeded) {
233                    result = SmsManager.MMS_ERROR_IO_ERROR;
234                }
235                pendingIntent.send(context, result, fillIn);
236            } catch (PendingIntent.CanceledException e) {
237                LogUtil.e(this.toString(), "Sending pending intent canceled", e);
238            }
239        }
240
241        revokeUriPermission(context);
242    }
243
244    /**
245     * Returns true if sending / downloading using the carrier app has failed and completes the
246     * action using platform API's, otherwise false.
247     */
248    protected boolean maybeFallbackToRegularDelivery(int carrierMessagingAppResult) {
249        if (carrierMessagingAppResult
250                == CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK
251                || carrierMessagingAppResult
252                        == CarrierMessagingService.DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK) {
253            LogUtil.d(this.toString(), "Sending/downloading MMS by IP failed.");
254            mRequestManager.addSimRequest(MmsRequest.this);
255            return true;
256        } else {
257            return false;
258        }
259    }
260
261    /**
262     * Converts from {@code carrierMessagingAppResult} to a platform result code.
263     */
264    protected static int toSmsManagerResult(int carrierMessagingAppResult) {
265        switch (carrierMessagingAppResult) {
266            case CarrierMessagingService.SEND_STATUS_OK:
267                return Activity.RESULT_OK;
268            case CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK:
269                return SmsManager.MMS_ERROR_RETRY;
270            default:
271                return SmsManager.MMS_ERROR_UNSPECIFIED;
272        }
273    }
274
275    @Override
276    public String toString() {
277        return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode());
278    }
279
280
281    protected String getRequestId() {
282        return this.toString();
283    }
284
285    /**
286     * Making the HTTP request to MMSC
287     *
288     * @param context The context
289     * @param netMgr The current {@link MmsNetworkManager}
290     * @param apn The APN setting
291     * @return The HTTP response data
292     * @throws MmsHttpException If any network error happens
293     */
294    protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn)
295            throws MmsHttpException;
296
297    /**
298     * @return The PendingIntent associate with the MMS sending invocation
299     */
300    protected abstract PendingIntent getPendingIntent();
301
302    /**
303     * @return The queue should be used by this request, 0 is sending and 1 is downloading
304     */
305    protected abstract int getQueueType();
306
307    /**
308     * Persist message into telephony if required (i.e. when auto-persisting is on or
309     * the calling app is non-default sms app for sending)
310     *
311     * @param context The context
312     * @param result The result code of execution
313     * @param response The response body
314     * @return The persisted URI of the message or null if we don't persist or fail
315     */
316    protected abstract Uri persistIfRequired(Context context, int result, byte[] response);
317
318    /**
319     * Prepare to make the HTTP request - will download message for sending
320     * @return true if preparation succeeds (and request can proceed) else false
321     */
322    protected abstract boolean prepareForHttpRequest();
323
324    /**
325     * Transfer the received response to the caller
326     *
327     * @param fillIn the intent that will be returned to the caller
328     * @param response the pdu to transfer
329     * @return true if response transfer succeeds else false
330     */
331    protected abstract boolean transferResponse(Intent fillIn, byte[] response);
332
333    /**
334     * Revoke the content URI permission granted by the MMS app to the phone package.
335     *
336     * @param context The context
337     */
338    protected abstract void revokeUriPermission(Context context);
339
340    /**
341     * Base class for handling carrier app send / download result.
342     */
343    protected abstract class CarrierMmsActionCallback extends ICarrierMessagingCallback.Stub {
344        @Override
345        public void onSendSmsComplete(int result, int messageRef) {
346            LogUtil.e("Unexpected onSendSmsComplete call with result: " + result);
347        }
348
349        @Override
350        public void onSendMultipartSmsComplete(int result, int[] messageRefs) {
351            LogUtil.e("Unexpected onSendMultipartSmsComplete call with result: " + result);
352        }
353
354        @Override
355        public void onFilterComplete(int result) {
356            LogUtil.e("Unexpected onFilterComplete call with result: " + result);
357        }
358    }
359}
360