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