1 /* 2 * Copyright (C) 2015 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 17 package com.android.messaging.datamodel.action; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.database.Cursor; 23 import android.net.ConnectivityManager; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.telephony.ServiceState; 27 28 import com.android.messaging.Factory; 29 import com.android.messaging.datamodel.BugleDatabaseOperations; 30 import com.android.messaging.datamodel.DataModel; 31 import com.android.messaging.datamodel.DatabaseHelper; 32 import com.android.messaging.datamodel.DatabaseHelper.MessageColumns; 33 import com.android.messaging.datamodel.DatabaseWrapper; 34 import com.android.messaging.datamodel.MessagingContentProvider; 35 import com.android.messaging.datamodel.data.MessageData; 36 import com.android.messaging.datamodel.data.ParticipantData; 37 import com.android.messaging.sms.MmsUtils; 38 import com.android.messaging.util.BugleGservices; 39 import com.android.messaging.util.BugleGservicesKeys; 40 import com.android.messaging.util.BuglePrefs; 41 import com.android.messaging.util.BuglePrefsKeys; 42 import com.android.messaging.util.ConnectivityUtil.ConnectivityListener; 43 import com.android.messaging.util.LogUtil; 44 import com.android.messaging.util.OsUtil; 45 import com.android.messaging.util.PhoneUtils; 46 47 import java.util.HashSet; 48 import java.util.Set; 49 50 /** 51 * Action used to lookup any messages in the pending send/download state and either fail them or 52 * retry their action. This action only initiates one retry at a time - further retries should be 53 * triggered by successful sending of a message, network status change or exponential backoff timer. 54 */ 55 public class ProcessPendingMessagesAction extends Action implements Parcelable { 56 private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; 57 private static final int PENDING_INTENT_REQUEST_CODE = 101; 58 59 public static void processFirstPendingMessage() { 60 // Clear any pending alarms or connectivity events 61 unregister(); 62 // Clear retry count 63 setRetry(0); 64 65 // Start action 66 final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); 67 action.start(); 68 } 69 70 public static void scheduleProcessPendingMessagesAction(final boolean failed, 71 final Action processingAction) { 72 LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages" 73 + (failed ? "(message failed)" : "")); 74 // Can safely clear any pending alarms or connectivity events as either an action 75 // is currently running or we will run now or register if pending actions possible. 76 unregister(); 77 78 final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp(); 79 boolean scheduleAlarm = false; 80 // If message succeeded and if Bugle is default SMS app just carry on with next message 81 if (!failed && isDefaultSmsApp) { 82 // Clear retry attempt count as something just succeeded 83 setRetry(0); 84 85 // Lookup and queue next message for immediate processing by background worker 86 // iff there are no pending messages this will do nothing and return true. 87 final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); 88 if (action.queueActions(processingAction)) { 89 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 90 if (processingAction.hasBackgroundActions()) { 91 LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued"); 92 } else { 93 LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue"); 94 } 95 } 96 // Have queued next action if needed, nothing more to do 97 return; 98 } 99 // In case of error queuing schedule a retry 100 scheduleAlarm = true; 101 LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying"); 102 } 103 if (getHavePendingMessages() || scheduleAlarm) { 104 // Still have a pending message that needs to be queued for processing 105 final ConnectivityListener listener = new ConnectivityListener() { 106 @Override 107 public void onConnectivityStateChanged(final Context context, final Intent intent) { 108 final int networkType = 109 MmsUtils.getConnectivityEventNetworkType(context, intent); 110 if (networkType != ConnectivityManager.TYPE_MOBILE) { 111 return; 112 } 113 final boolean isConnected = !intent.getBooleanExtra( 114 ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); 115 // TODO: Should we check in more detail? 116 if (isConnected) { 117 onConnected(); 118 } 119 } 120 121 @Override 122 public void onPhoneStateChanged(final Context context, final int serviceState) { 123 if (serviceState == ServiceState.STATE_IN_SERVICE) { 124 onConnected(); 125 } 126 } 127 128 private void onConnected() { 129 LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected; starting action"); 130 131 // Clear any pending alarms or connectivity events but leave attempt count alone 132 unregister(); 133 134 // Start action 135 final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); 136 action.start(); 137 } 138 }; 139 // Read and increment attempt number from shared prefs 140 final int retryAttempt = getNextRetry(); 141 register(listener, retryAttempt); 142 } else { 143 // No more pending messages (presumably the message that failed has expired) or it 144 // may be possible that a send and a download are already in process. 145 // Clear retry attempt count. 146 // TODO Might be premature if send and download in process... 147 // but worst case means we try to send a bit more often. 148 setRetry(0); 149 150 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 151 LogUtil.v(TAG, "ProcessPendingMessagesAction: No more pending messages"); 152 } 153 } 154 } 155 156 private static void register(final ConnectivityListener listener, final int retryAttempt) { 157 int retryNumber = retryAttempt; 158 159 // Register to be notified about connectivity changes 160 DataModel.get().getConnectivityUtil().register(listener); 161 162 final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); 163 final long initialBackoffMs = BugleGservices.get().getLong( 164 BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS, 165 BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT); 166 final long maxDelayMs = BugleGservices.get().getLong( 167 BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS, 168 BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT); 169 long delayMs; 170 long nextDelayMs = initialBackoffMs; 171 do { 172 delayMs = nextDelayMs; 173 retryNumber--; 174 nextDelayMs = delayMs * 2; 175 } 176 while (retryNumber > 0 && nextDelayMs < maxDelayMs); 177 178 LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt 179 + " in " + delayMs + " ms"); 180 181 action.schedule(PENDING_INTENT_REQUEST_CODE, delayMs); 182 } 183 184 private static void unregister() { 185 // Clear any pending alarms or connectivity events 186 DataModel.get().getConnectivityUtil().unregister(); 187 188 final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction(); 189 action.schedule(PENDING_INTENT_REQUEST_CODE, Long.MAX_VALUE); 190 191 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 192 LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed " 193 + "events and clearing scheduled alarm"); 194 } 195 } 196 197 private static void setRetry(final int retryAttempt) { 198 final BuglePrefs prefs = Factory.get().getApplicationPrefs(); 199 prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt); 200 } 201 202 private static int getNextRetry() { 203 final BuglePrefs prefs = Factory.get().getApplicationPrefs(); 204 final int retryAttempt = 205 prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1; 206 prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt); 207 return retryAttempt; 208 } 209 210 private ProcessPendingMessagesAction() { 211 } 212 213 /** 214 * Read from the DB and determine if there are any messages we should process 215 * @return true if we have pending messages 216 */ 217 private static boolean getHavePendingMessages() { 218 final DatabaseWrapper db = DataModel.get().getDatabase(); 219 final long now = System.currentTimeMillis(); 220 221 final String toSendMessageId = findNextMessageToSend(db, now); 222 if (toSendMessageId != null) { 223 return true; 224 } else { 225 final String toDownloadMessageId = findNextMessageToDownload(db, now); 226 if (toDownloadMessageId != null) { 227 return true; 228 } 229 } 230 // Messages may be in the process of sending/downloading even when there are no pending 231 // messages... 232 return false; 233 } 234 235 /** 236 * Queue any pending actions 237 * @param actionState 238 * @return true if action queued (or no actions to queue) else false 239 */ 240 private boolean queueActions(final Action processingAction) { 241 final DatabaseWrapper db = DataModel.get().getDatabase(); 242 final long now = System.currentTimeMillis(); 243 boolean succeeded = true; 244 245 // Will queue no more than one message to send plus one message to download 246 // This keeps outgoing messages "in order" but allow downloads to happen even if sending 247 // gets blocked until messages time out. Manual resend bumps messages to head of queue. 248 final String toSendMessageId = findNextMessageToSend(db, now); 249 final String toDownloadMessageId = findNextMessageToDownload(db, now); 250 if (toSendMessageId != null) { 251 LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId 252 + " for sending"); 253 // This could queue nothing 254 if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) { 255 LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " 256 + toSendMessageId + " for sending"); 257 succeeded = false; 258 } 259 } 260 if (toDownloadMessageId != null) { 261 LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId 262 + " for download"); 263 // This could queue nothing 264 if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId, 265 processingAction)) { 266 LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message " 267 + toDownloadMessageId + " for download"); 268 succeeded = false; 269 } 270 } 271 if (toSendMessageId == null && toDownloadMessageId == null) { 272 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 273 LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download"); 274 } 275 } 276 return succeeded; 277 } 278 279 @Override 280 protected Object executeAction() { 281 // If triggered by alarm will not have unregistered yet 282 unregister(); 283 284 if (PhoneUtils.getDefault().isDefaultSmsApp()) { 285 queueActions(this); 286 } else { 287 if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { 288 LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling"); 289 } 290 scheduleProcessPendingMessagesAction(true, this); 291 } 292 293 return null; 294 } 295 296 private static String findNextMessageToSend(final DatabaseWrapper db, final long now) { 297 String toSendMessageId = null; 298 db.beginTransaction(); 299 Cursor sending = null; 300 Cursor cursor = null; 301 int sendingCnt = 0; 302 int pendingCnt = 0; 303 int failedCnt = 0; 304 try { 305 // First check to see if we have any messages already sending 306 sending = db.query(DatabaseHelper.MESSAGES_TABLE, 307 MessageData.getProjection(), 308 DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", 309 new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING), 310 Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)}, 311 null, 312 null, 313 DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); 314 final boolean messageCurrentlySending = sending.moveToNext(); 315 sendingCnt = sending.getCount(); 316 // Look for messages we could send 317 final ContentValues values = new ContentValues(); 318 values.put(DatabaseHelper.MessageColumns.STATUS, 319 MessageData.BUGLE_STATUS_OUTGOING_FAILED); 320 cursor = db.query(DatabaseHelper.MESSAGES_TABLE, 321 MessageData.getProjection(), 322 DatabaseHelper.MessageColumns.STATUS + " IN (" 323 + MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + "," 324 + MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")", 325 null, 326 null, 327 null, 328 DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); 329 pendingCnt = cursor.getCount(); 330 331 while (cursor.moveToNext()) { 332 final MessageData message = new MessageData(); 333 message.bind(cursor); 334 if (message.getInResendWindow(now)) { 335 // If no messages currently sending 336 if (!messageCurrentlySending) { 337 // Resend this message 338 toSendMessageId = message.getMessageId(); 339 // Before queuing the message for resending, check if the message's self is 340 // active. If not, switch back to the system's default subscription. 341 if (OsUtil.isAtLeastL_MR1()) { 342 final ParticipantData messageSelf = BugleDatabaseOperations 343 .getExistingParticipant(db, message.getSelfId()); 344 if (messageSelf == null || !messageSelf.isActiveSubscription()) { 345 final ParticipantData defaultSelf = BugleDatabaseOperations 346 .getOrCreateSelf(db, PhoneUtils.getDefault() 347 .getDefaultSmsSubscriptionId()); 348 if (defaultSelf != null) { 349 message.bindSelfId(defaultSelf.getId()); 350 final ContentValues selfValues = new ContentValues(); 351 selfValues.put(MessageColumns.SELF_PARTICIPANT_ID, 352 defaultSelf.getId()); 353 BugleDatabaseOperations.updateMessageRow(db, 354 message.getMessageId(), selfValues); 355 MessagingContentProvider.notifyMessagesChanged( 356 message.getConversationId()); 357 } 358 } 359 } 360 } 361 break; 362 } else { 363 failedCnt++; 364 365 // Mark message as failed 366 BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values); 367 MessagingContentProvider.notifyMessagesChanged(message.getConversationId()); 368 } 369 } 370 db.setTransactionSuccessful(); 371 } finally { 372 db.endTransaction(); 373 if (cursor != null) { 374 cursor.close(); 375 } 376 if (sending != null) { 377 sending.close(); 378 } 379 } 380 381 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 382 LogUtil.d(TAG, "ProcessPendingMessagesAction: " 383 + sendingCnt + " messages already sending, " 384 + pendingCnt + " messages to send, " 385 + failedCnt + " failed messages"); 386 } 387 388 return toSendMessageId; 389 } 390 391 private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) { 392 String toDownloadMessageId = null; 393 db.beginTransaction(); 394 Cursor cursor = null; 395 int downloadingCnt = 0; 396 int pendingCnt = 0; 397 try { 398 // First check if we have any messages already downloading 399 downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE, 400 DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)", 401 new String[] { 402 Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING), 403 Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) 404 }); 405 406 // TODO: This query is not actually needed if downloadingCnt == 0. 407 cursor = db.query(DatabaseHelper.MESSAGES_TABLE, 408 MessageData.getProjection(), 409 DatabaseHelper.MessageColumns.STATUS + " =? OR " 410 + DatabaseHelper.MessageColumns.STATUS + " =?", 411 new String[]{ 412 Integer.toString( 413 MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD), 414 Integer.toString( 415 MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD) 416 }, 417 null, 418 null, 419 DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC"); 420 421 pendingCnt = cursor.getCount(); 422 423 // If no messages are currently downloading and there is a download pending, 424 // queue the download of the oldest pending message. 425 if (downloadingCnt == 0 && cursor.moveToNext()) { 426 // Always start the next pending message. We will check if a download has 427 // expired in DownloadMmsAction and mark message failed there. 428 final MessageData message = new MessageData(); 429 message.bind(cursor); 430 toDownloadMessageId = message.getMessageId(); 431 } 432 db.setTransactionSuccessful(); 433 } finally { 434 db.endTransaction(); 435 if (cursor != null) { 436 cursor.close(); 437 } 438 } 439 440 if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { 441 LogUtil.d(TAG, "ProcessPendingMessagesAction: " 442 + downloadingCnt + " messages already downloading, " 443 + pendingCnt + " messages to download"); 444 } 445 446 return toDownloadMessageId; 447 } 448 449 private ProcessPendingMessagesAction(final Parcel in) { 450 super(in); 451 } 452 453 public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR 454 = new Parcelable.Creator<ProcessPendingMessagesAction>() { 455 @Override 456 public ProcessPendingMessagesAction createFromParcel(final Parcel in) { 457 return new ProcessPendingMessagesAction(in); 458 } 459 460 @Override 461 public ProcessPendingMessagesAction[] newArray(final int size) { 462 return new ProcessPendingMessagesAction[size]; 463 } 464 }; 465 466 @Override 467 public void writeToParcel(final Parcel parcel, final int flags) { 468 writeActionToParcel(parcel, flags); 469 } 470 } 471