1/*
2 * Copyright (C) 2008 Esmertec AG.
3 * Copyright (C) 2008 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mms.transaction;
19
20import com.android.mms.R;
21import com.android.mms.LogTag;
22import com.android.mms.util.DownloadManager;
23import com.google.android.mms.pdu.PduHeaders;
24import com.google.android.mms.pdu.PduPersister;
25import android.database.sqlite.SqliteWrapper;
26
27import android.app.AlarmManager;
28import android.app.PendingIntent;
29import android.content.ContentResolver;
30import android.content.ContentUris;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.Intent;
34import android.database.Cursor;
35import android.net.ConnectivityManager;
36import android.net.NetworkInfo;
37import android.net.Uri;
38import android.provider.Telephony.Mms;
39import android.provider.Telephony.MmsSms;
40import android.provider.Telephony.Sms;
41import android.provider.Telephony.MmsSms.PendingMessages;
42import android.text.format.DateFormat;
43import android.util.Log;
44
45public class RetryScheduler implements Observer {
46    private static final String TAG = "RetryScheduler";
47    private static final boolean DEBUG = false;
48    private static final boolean LOCAL_LOGV = false;
49
50    private final Context mContext;
51    private final ContentResolver mContentResolver;
52
53    private RetryScheduler(Context context) {
54        mContext = context;
55        mContentResolver = context.getContentResolver();
56    }
57
58    private static RetryScheduler sInstance;
59    public static RetryScheduler getInstance(Context context) {
60        if (sInstance == null) {
61            sInstance = new RetryScheduler(context);
62        }
63        return sInstance;
64    }
65
66    private boolean isConnected() {
67        ConnectivityManager mConnMgr = (ConnectivityManager)
68                mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
69        NetworkInfo ni = mConnMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
70        return (ni == null ? false : ni.isConnected());
71    }
72
73    public void update(Observable observable) {
74        try {
75            Transaction t = (Transaction) observable;
76
77            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
78                Log.v(TAG, "[RetryScheduler] update " + observable);
79            }
80
81            // We are only supposed to handle M-Notification.ind, M-Send.req
82            // and M-ReadRec.ind.
83            if ((t instanceof NotificationTransaction)
84                    || (t instanceof RetrieveTransaction)
85                    || (t instanceof ReadRecTransaction)
86                    || (t instanceof SendTransaction)) {
87                try {
88                    TransactionState state = t.getState();
89                    if (state.getState() == TransactionState.FAILED) {
90                        Uri uri = state.getContentUri();
91                        if (uri != null) {
92                            scheduleRetry(uri);
93                        }
94                    }
95                } finally {
96                    t.detach(this);
97                }
98            }
99        } finally {
100            if (isConnected()) {
101                setRetryAlarm(mContext);
102            }
103        }
104    }
105
106    private void scheduleRetry(Uri uri) {
107        long msgId = ContentUris.parseId(uri);
108
109        Uri.Builder uriBuilder = PendingMessages.CONTENT_URI.buildUpon();
110        uriBuilder.appendQueryParameter("protocol", "mms");
111        uriBuilder.appendQueryParameter("message", String.valueOf(msgId));
112
113        Cursor cursor = SqliteWrapper.query(mContext, mContentResolver,
114                uriBuilder.build(), null, null, null, null);
115
116        if (cursor != null) {
117            try {
118                if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
119                    int msgType = cursor.getInt(cursor.getColumnIndexOrThrow(
120                            PendingMessages.MSG_TYPE));
121
122                    int retryIndex = cursor.getInt(cursor.getColumnIndexOrThrow(
123                            PendingMessages.RETRY_INDEX)) + 1; // Count this time.
124
125                    // TODO Should exactly understand what was happened.
126                    int errorType = MmsSms.ERR_TYPE_GENERIC;
127
128                    DefaultRetryScheme scheme = new DefaultRetryScheme(mContext, retryIndex);
129
130                    ContentValues values = new ContentValues(4);
131                    long current = System.currentTimeMillis();
132                    boolean isRetryDownloading =
133                            (msgType == PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
134                    boolean retry = true;
135                    int respStatus = getResponseStatus(msgId);
136                    int errorString = 0;
137                    switch (respStatus) {
138                        case PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED:
139                            errorString = R.string.invalid_destination;
140                            break;
141
142                        case PduHeaders.RESPONSE_STATUS_ERROR_SERVICE_DENIED:
143                        case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED:
144                            errorString = R.string.service_not_activated;
145                            break;
146
147                        case PduHeaders.RESPONSE_STATUS_ERROR_NETWORK_PROBLEM:
148                            errorString = R.string.service_network_problem;
149                            break;
150
151                        case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND:
152                        case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND:
153                            errorString = R.string.service_message_not_found;
154                            break;
155                    }
156                    if (errorString != 0) {
157                        DownloadManager.getInstance().showErrorCodeToast(errorString);
158                        retry = false;
159                    }
160
161                    if ((retryIndex < scheme.getRetryLimit()) && retry) {
162                        long retryAt = current + scheme.getWaitingInterval();
163
164                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
165                            Log.v(TAG, "scheduleRetry: retry for " + uri + " is scheduled at "
166                                    + (retryAt - System.currentTimeMillis()) + "ms from now");
167                        }
168
169                        values.put(PendingMessages.DUE_TIME, retryAt);
170
171                        if (isRetryDownloading) {
172                            // Downloading process is transiently failed.
173                            DownloadManager.getInstance().markState(
174                                    uri, DownloadManager.STATE_TRANSIENT_FAILURE);
175                        }
176                    } else {
177                        errorType = MmsSms.ERR_TYPE_GENERIC_PERMANENT;
178                        if (isRetryDownloading) {
179                            Cursor c = SqliteWrapper.query(mContext, mContext.getContentResolver(), uri,
180                                    new String[] { Mms.THREAD_ID }, null, null, null);
181
182                            long threadId = -1;
183                            if (c != null) {
184                                try {
185                                    if (c.moveToFirst()) {
186                                        threadId = c.getLong(0);
187                                    }
188                                } finally {
189                                    c.close();
190                                }
191                            }
192
193                            if (threadId != -1) {
194                                // Downloading process is permanently failed.
195                                MessagingNotification.notifyDownloadFailed(mContext, threadId);
196                            }
197
198                            DownloadManager.getInstance().markState(
199                                    uri, DownloadManager.STATE_PERMANENT_FAILURE);
200                        } else {
201                            // Mark the failed message as unread.
202                            ContentValues readValues = new ContentValues(1);
203                            readValues.put(Mms.READ, 0);
204                            SqliteWrapper.update(mContext, mContext.getContentResolver(),
205                                    uri, readValues, null, null);
206                            MessagingNotification.notifySendFailed(mContext, true);
207                        }
208                    }
209
210                    values.put(PendingMessages.ERROR_TYPE,  errorType);
211                    values.put(PendingMessages.RETRY_INDEX, retryIndex);
212                    values.put(PendingMessages.LAST_TRY,    current);
213
214                    int columnIndex = cursor.getColumnIndexOrThrow(
215                            PendingMessages._ID);
216                    long id = cursor.getLong(columnIndex);
217                    SqliteWrapper.update(mContext, mContentResolver,
218                            PendingMessages.CONTENT_URI,
219                            values, PendingMessages._ID + "=" + id, null);
220                } else if (LOCAL_LOGV) {
221                    Log.v(TAG, "Cannot found correct pending status for: " + msgId);
222                }
223            } finally {
224                cursor.close();
225            }
226        }
227    }
228
229    private int getResponseStatus(long msgID) {
230        int respStatus = 0;
231        Cursor cursor = SqliteWrapper.query(mContext, mContentResolver,
232                Mms.Outbox.CONTENT_URI, null, Mms._ID + "=" + msgID, null, null);
233        try {
234            if (cursor.moveToFirst()) {
235                respStatus = cursor.getInt(cursor.getColumnIndexOrThrow(Mms.RESPONSE_STATUS));
236            }
237        } finally {
238            cursor.close();
239        }
240        if (respStatus != 0) {
241            Log.e(TAG, "Response status is: " + respStatus);
242        }
243        return respStatus;
244    }
245
246    public static void setRetryAlarm(Context context) {
247        Cursor cursor = PduPersister.getPduPersister(context).getPendingMessages(
248                Long.MAX_VALUE);
249        if (cursor != null) {
250            try {
251                if (cursor.moveToFirst()) {
252                    // The result of getPendingMessages() is order by due time.
253                    long retryAt = cursor.getLong(cursor.getColumnIndexOrThrow(
254                            PendingMessages.DUE_TIME));
255
256                    Intent service = new Intent(TransactionService.ACTION_ONALARM,
257                                        null, context, TransactionService.class);
258                    PendingIntent operation = PendingIntent.getService(
259                            context, 0, service, PendingIntent.FLAG_ONE_SHOT);
260                    AlarmManager am = (AlarmManager) context.getSystemService(
261                            Context.ALARM_SERVICE);
262                    am.set(AlarmManager.RTC, retryAt, operation);
263
264                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
265                        Log.v(TAG, "Next retry is scheduled at"
266                                + (retryAt - System.currentTimeMillis()) + "ms from now");
267                    }
268                }
269            } finally {
270                cursor.close();
271            }
272        }
273    }
274}
275